在WinForms应用程序中的多线程处理

4
我正在编写一个Win Forms应用程序,使用报表查看器创建多个PDF文件。这些PDF文件分为4个主要部分,每个部分负责创建特定的报告。这些进程将创建至少1个文件,最多可达到用户数量(目前为50)。
该程序已经存在并按顺序使用了这4种方法。为了在用户数量增加时获得额外的性能,我想将这些方法从主线程中分离出来,以4个单独的线程运行。
虽然我是C#中的新手,但我阅读了许多关于如何实现多线程的文章。唯一我不确定的是应该从哪里开始。由于我阅读了多篇博客文章,我不确定是要使用4个单独的线程,线程池还是多个后台工作者。(或者并行编程是最好的方法?)。博客文章告诉我,如果使用3个以上的线程,请使用线程池,但另一方面,他们告诉我,如果使用WinForms,请使用BackgroundWorker。哪个选项最好(为什么)?
最后,在继续之前,我的主线程必须等待所有进程结束。
有人可以告诉我解决问题的最佳方案吗?
*编辑后的额外信息*
我忘记告诉大家(在阅读了您所有的评论和可能的解决方案之后)。这些方法仅共享一个“IEnumerable”以供读取。在触发这些方法(不必按顺序运行)之后,这些方法会触发事件以向UI发送状态更新。我认为,如果使用单独的线程触发事件将会很困难,甚至不可能,因此应该有某种回调函数来报告运行时的状态更新。
以下是一些伪代码示例。
 main()
 {
       private List<customclass> lcc = importCustomClass()

       export.CreatePDFKind1.create(lcc.First(), exportfolderpath, arg1)

       export.CreatePDFKind2.create(lcc, exportfolderpath)

       export.CreatePDFKind3.create(lcc.First(), exportfolderpath) 

       export.CreatePDFKind4.create(customclass2, exportfolderpath)
 } 

 namespace export
 {
     class CreatePDFKind1         
     {
        create(customclass cc, string folderpath)
        {
            do something;
            reportstatus(listviewItem, status, message)
        }
     }

     class CreatePDFKind2
     {
        create(IEnumerable<customclass> lcc, string folderpath)
        {
            foreach (var x in lcc)
            {
               do something;
               reportstatus(listviewItem, status, message)
            }
        }
     }

     etc.......
  }

1
人们通常建议使用BackgroundWorker在另一个线程上执行一些长时间运行的操作,以保持UI的响应性。但在这里它可能不是最好的选择,因为你需要多个线程。TPL可能是更好的选择。 - Rob P.
如果这四个操作是顺序执行的,那么你只需要一个线程。在这种情况下,我会使用后台工作器。 - 001
3个回答

5
从你描述的基本情况来看,我建议使用任务并行库(TPL)。它随 .NET Framework 4.0+ 一起发布。
你谈到了在生成大到中等规模的线程时使用线程池的“最佳”选项。尽管这是正确的(管理资源的最有效方式),但 TPL 可以帮你完成所有这些工作,而无需你操心。TPL 还使得使用多个线程和等待它们完成变得非常容易...
为了完成你的要求,我会使用TPLContinuations。Continuation 不仅可以创建任务流,还可以处理异常。这篇优秀的介绍文章可以给你一些想法...
你可以使用以下方式启动一个 TPL 任务:
Task task = Task.Factory.StartNew(() => 
{
    // Do some work here...
});

现在,当前置任务完成(成功或出错)后,您可以使用ContinueWith方法启动第二个任务。
Task task1 = Task.Factory.StartNew(() => Console.WriteLine("Antecedant Task"));
Task task2 = task1.ContinueWith(antTask => Console.WriteLine("Continuation..."));

所以一旦task1完成、失败或被取消,task2就会“启动”并开始运行。请注意,如果task1在第二行代码之前已经完成,task2将立即被安排执行。传递给第二个lambda的antTask参数是指前置任务的引用。有关更详细的示例,请参见此链接...
您还可以从前置任务中传递连续结果。
Task.Factory.StartNew<int>(() => 1)
    .ContinueWith(antTask => antTask.Result * 4)
    .ContinueWith(antTask => antTask.Result * 4)
    .ContinueWith(antTask =>Console.WriteLine(antTask.Result * 4)); // Prints 64.

注意。一定要阅读第一个链接中关于异常处理的内容,因为这可能会让TPL的新手走上歧途。

最后一个需要特别注意的是子任务。子任务是使用AttachedToParent创建的任务。在这种情况下,继续运行直到所有子任务完成。

TaskCreationOptions atp = TaskCreationOptions.AttachedToParent;
Task.Factory.StartNew(() =>
{
    Task.Factory.StartNew(() => { SomeMethod() }, atp);
    Task.Factory.StartNew(() => { SomeOtherMethod() }, atp); 
}).ContinueWith( cont => { Console.WriteLine("Finished!") });

在你的情况下,你会开始执行四个任务,然后在主线程上等待它们完成。

希望这能帮到你。


1

如果您需要与后台进程交互并涉及到UI,使用BackgroundWorker会很有帮助。如果不需要,则可以直接启动4个Task对象,无需使用BackgroundWorker

tasks.Add(Task.Factory.StartNew(()=>DoStuff()));
tasks.Add(Task.Factory.StartNew(()=>DoStuff2()));
tasks.Add(Task.Factory.StartNew(()=>DoStuff3()));

如果您确实需要与UI交互,可能需要更新它以反映任务何时完成,那么我建议启动一个BackgroundWorker,然后再次使用任务来处理每个单独的工作单元。由于使用BackgroundWorker会有一些额外的开销,因此如果可以避免,我会避免启动大量的BackgroundWorker。
BackgroundWorker bgw = new BackgroundWorker();
bgw.DoWork += (_, args) =>
{
    List<Task> tasks = new List<Task>();

    tasks.Add(Task.Factory.StartNew(() => DoStuff()));
    tasks.Add(Task.Factory.StartNew(() => DoStuff2()));
    tasks.Add(Task.Factory.StartNew(() => DoStuff3()));

    Task.WaitAll(tasks.ToArray());
};
bgw.RunWorkerCompleted += (_, args) => updateUI();
bgw.RunWorkerAsync();

当然,您可以仅使用Task方法来完成所有这些操作,但我仍然发现BackgroundWorkers在简单情况下更容易使用。使用.NET 4.5,您可以使用Task.WhenAll在所有4个任务完成时在UI线程中运行一个继续,但在4.0中这样做不会那么简单。

1
为了更新UI,为什么不使用continuation呢?使用TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(),然后将其传递给continuation,这将允许您更新UI... - MoonKnight
@Killercam 如果你有多个任务,使用4.0并不是那么简单。你真正需要的是"WhenAll",它是在4.5中添加的(如果你有4.5,最好只是在其上使用async/await)。就像我说的,这是可以做到的,只是没有那么直接。使用TPL处理进度报告也比使用BGW不那么简单。 - Servy

0

没有更多的信息,很难判断。如果它们正在访问相同的资源,那么它们在四个不同的方法中并没有太大的区别。例如PDF文件。如果您无法理解我的意思,您应该发布每种方法的一些代码,我会详细说明。

由于您拥有的“部分”数量是固定的,因此使用单独的线程、后台工作者或使用线程池都不会有太大的区别。我不确定为什么人们推荐后台工作者。最可能是因为这是一种更简单的多线程方法,更难出错。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接