在控制台应用程序中使用.NET BackgroundWorker类

14

我在.NET编程和多线程方面相对新手,想知道是否可以在控制台应用程序中使用.NET提供的BackgroundWorker来生成工作线程来做一些工作?从网上的各种文档中看到,这个类的意图更多地是为面向UI的应用程序设计的,其中你想在后台执行一些工作,但保持UI响应,并报告进度、必要时取消处理等。

在我的情况下,我有一个控制器类,想要生成多个工作线程来进行一些处理(使用信号量限制最大数量的工作线程)。然后我希望我的控制器类在所有线程完成处理之前阻塞。所以在启动工作线程来处理工作后,我希望线程可以在处理完成时通知控制器线程。我看到可以使用BackgroundWorker类,并处理DoWork和RunWorkerCompleted事件来完成这一点,但是想知道是否这是一个好主意?是否有更好的方法实现这一点?


2
虽然它可能无法帮助您解决此问题,但 .NET Framework 4 的新并行编程功能可能会引起您的兴趣。这是一件值得关注的事情 - 它很快就会发布。http://msdn.microsoft.com/en-us/concurrency/default.aspx - Andy West
4个回答

9

如果您的需求只是等待所有线程完成,那么这很容易 - 只需启动新线程,然后在每个线程上调用Thread.Join

using System;
using System.Collections.Generic;
using System.Threading;

public class Test
{
    static void Main()
    {
        var threads = new List<Thread>();
        for (int i = 0; i < 10; i++)
        {
            int copy = i;
            Thread thread = new Thread(() => DoWork(copy));
            thread.Start();
            threads.Add(thread);
        }

        Console.WriteLine("Main thread blocking");
        foreach (Thread thread in threads)
        {
            thread.Join();
        }
        Console.WriteLine("Main thread finished");
    }

    static void DoWork(int thread)
    {
        Console.WriteLine("Thread {0} doing work", thread);
        Random rng = new Random(thread); // Seed with unique numbers
        Thread.Sleep(rng.Next(2000));
        Console.WriteLine("Thread {0} done", thread);
    }
}

编辑:如果您可以访问.NET 4.0,则TPL绝对是正确的选择。否则,我建议使用生产者/消费者队列(有大量示例代码可用)。基本上,您拥有一个工作项队列和与核心数相同的消费者线程(假设它们是CPU密集型;您需要根据工作负载进行调整)。每个消费者线程将从队列中获取一个项目并逐个处理。如何管理这一点将取决于您的情况,但它并不是非常复杂。如果您能提前想出所有需要完成的工作,那么当线程发现队列为空时,它们就可以退出,这样会更容易些。


这会比使用线程池线程增加相当多的启动时间。启动自己的线程并不快,而且可能是不必要的。 - Reed Copsey
1
@Reed:除非线程池已经预热,否则成本仍将存在...而且启动线程的成本真的*不高。在我的netbook上,启动和加入1000个线程不到1秒钟。我并不认为这是"相当多的启动时间"——而且很可能OP也不需要1000个线程。我认为这是比使用线程池更简单的解决方案。 - Jon Skeet
1
@Jon:没错,但如果你生成了1000个线程,并且这1000个线程都在工作,你会过度使用系统,导致非常严重的抖动。线程池限制运行线程数量是有原因的(.NET 4中的TPL也非常擅长此项工作,因为它根据工作负载在运行时调整总线程数)。OP明确表示他想要限制运行线程的总数,这对我来说意味着他可能正在生成大量的工作项... - Reed Copsey
实际上这正是我希望线程在操作完成后通知主线程的原因。例如,如果我有1000个工作单元,并且正在使用10个线程来处理工作。因此,在每个线程完成工作项时,我希望生成另一个线程来获取下一个待处理的工作项。这样,我希望将同时运行的线程数限制为10个。 - jvtech
@jvtech:我刚刚更新了我的答案,提供了一种更简单的替代方案... - Reed Copsey
显示剩余3条评论

8

在控制台应用程序中,这不起作用,因为 SynchronizationContext.Current永远不会初始化。只有在使用GUI应用程序时,Windows Forms或WPF才会为您初始化它。

话虽如此,没有理由这样做。只需使用 ThreadPool.QueueUserWorkItem和重置事件( ManualResetEvent AutoResetEvent)来捕获完成状态并阻止主线程即可。


编辑:

看到一些原帖评论后,我想补充一下。

在我看来,“最好”的选择是获取Rx Framework的副本,因为它包含了.NET 4中TPL的回溯。这将允许您使用Parallel.ForEach的重载,该重载提供了提供ParallelOptions实例的选项。这将允许您限制总并发操作数,并为您处理所有工作:

// using collection of work items, such as: List<Action<object>> workItems;
var options = new ParallelOptions();
options.MaxDegreeOfParallelism = 10; // Restrict to 10 threads, not recommended!

// Perform all actions in the list, in parallel
Parallel.ForEach(workItems, options, item => { item(null); });

然而,使用Parallel.ForEach时,我个人会让系统管理并行度。它将自动分配适当数量的线程(特别是当/如果它转移到.NET 4时)。


2
很抱歉打扰到您,但是在2022年,这个答案在“控制台应用程序工作线程”方面排名第一。我刚刚开始使用C#/.Net,上周才开始接触(之前主要使用C++),所以可能会漏掉一些东西。但是...它似乎可以工作。
使用.NET 5.0(不了解其他框架)。 (示例有一些偏执和不耐烦的倾向。)
using System.ComponentModel;  // for BackgroundWorker
using Console = System.Console;

class Program
{
  static void Main(string[] args) {
    BackgroundWorker worker = new();
    worker.DoWork += worker_DoWork;
    worker.RunWorkerCompleted += worker_RunWorkerCompleted;
    worker.ProgressChanged += worker_ProgressChanged;
    worker.WorkerReportsProgress = true;
    worker.WorkerSupportsCancellation = true;

    Console.WriteLine("Starting worker... (any key to cancel/exit)");

    worker.RunWorkerAsync();

    Console.ReadKey(true);  // event loop

    if (worker.IsBusy) {
      Console.WriteLine("Interrupting the worker...");
      worker.CancelAsync();
      var sw = System.Diagnostics.Stopwatch.StartNew();
      while (worker.IsBusy && sw.ElapsedMilliseconds < 5000)
        System.Threading.Thread.Sleep(1);
    }
  }

  static void worker_ProgressChanged(object _, ProgressChangedEventArgs e) {
    Console.WriteLine("Worker progress: {0:d}%", e.ProgressPercentage);
  }

  static void worker_DoWork(object sender, DoWorkEventArgs e) {
    Console.WriteLine("Worker: Starting to do some work now...");
    BackgroundWorker worker = sender as BackgroundWorker;

    e.Result = 0;
    for (int i = 1; i < 11; ++i) {
      for (int ii=0; !worker.CancellationPending && ii < 10; ++ii)
        System.Threading.Thread.Sleep(100);
      if (worker.CancellationPending)
        break;
      worker.ReportProgress((int)((100.0 * i) / 10));
      e.Result = i;
    }
    e.Cancel = worker.CancellationPending;
  }

  static void worker_RunWorkerCompleted(object _, RunWorkerCompletedEventArgs e) {
    if (e.Cancelled) {
      Console.WriteLine("Worker: I wuz busy!");
      return;
    }

    Console.WriteLine("Worker: I worked {0:D} times.", e.Result);
    Console.WriteLine("Worker: Done now!");
  }
}

最初可在https://www.codeproject.com/Questions/625473/backgroundWorker-in-csharp 找到(发布于2013年7月,因此我认为这已经运作了一段时间?)。


该链接是与C#中的backgroundWorker相关的技术内容。

也适用于.NET框架4.8 - JeremyW

1
你说得对,BackgroundWorker在这里不起作用。它确实是为GUI环境(如WinForms和WPF)设计的,其中主线程永久忙于检查/执行消息泵(处理所有Windows事件,如单击和调整大小,以及来自BackgroundWorker的事件)。没有事件队列,主线程会耗尽。而没有事件队列,BackgroundWorker也无法在主线程上调用回调函数。
在控制台上进行多线程确实比在GUI环境中更加困难,因为你没有消息泵来解决两个重要问题:如何保持主线程活动状态以及如何通知主线程需要UI上的变化。基于事件的编程被证明是多线程理想的学习场所。
有一些解决方案:
扩展你的控制台应用程序,使用消息泵。由于它只是一个线程共享的委托集合(其他线程仅添加内容)和while循环,因此它非常容易实现: C#控制台应用程序+事件处理 线程池和线程类。它们就像BackgroundWorker一样存在(自.NET 2.0以来),似乎是为解决控制台的问题而设计的。您仍然需要使用循环阻止主线程。
线程池还将限制线程数,适用于真实机器。您应该避免固定的线程限制,而是依赖OS的线程池功能来处理“同时运行多少线程”的问题。
例如,5个线程的固定数量可能非常适合您的6核开发机器。但对于单核桌面计算机来说则太多了,对于有16到64个内核和大量RAM可用的服务器来说,则是一个毫无意义的瓶颈。
如果您拥有.NET 4.0或更高版本,则新添加的任务并行性可能值得一看。 http://msdn.microsoft.com/en-us/library/dd537609.aspx 它具有内置的线程池、有限的优先级能力,可以轻松地链式和合并多个任务(所有线程都可以做到的事情,任务也可以做到)。 使用任务还可以使用异步和等待关键字: http://msdn.microsoft.com/en-us/library/hh191443.aspx 同样,在控制台应用程序中无法避免阻塞主线程。

实际上,事件非常容易使用,是一种高效的方式,可以使控制台程序一直执行,直到发生特定的事情。 - Sam Hobbs

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