【问题标题】:Using .NET BackgroundWorker class in console app在控制台应用程序中使用 .NET BackgroundWorker 类
【发布时间】:2010-02-03 23:06:09
【问题描述】:

总的来说,我对 .NET 编程和多线程比较陌生,想知道是否可以使用 .NET 提供的 BackgroundWorker 来生成工作线程以在控制台应用程序中执行某些工作?从在线的各种文档中,我看到这个类的意图更多的是面向 UI 的应用程序,你想在后台做一些工作,但保持 UI 响应,并报告进度,如果需要取消处理等。

就我而言,基本上我有一个控制器类,我想在其中生成多个工作线程以进行一些处理(限制使用信号量生成的工作线程的最大数量)。然后我希望我的控制器类阻塞,直到所有线程都完成处理。所以在我启动一个工作线程做一些工作之后,我希望线程能够在处理完成时通知控制器线程。我看到我可以使用后台工作类,并处理 DoWork 和 RunWorkerCompleted 事件来完成此任务,但是想知道这是否是个好主意?有没有更好的方法来实现这一点?

【问题讨论】:

标签: .net multithreading backgroundworker


【解决方案1】:

如果您的要求只是阻塞直到所有线程都完成,那真的很容易 - 只需启动新线程,然后在每个线程上调用 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 限制;您希望根据您的工作负载对其进行定制)。每个消费者线程都会从队列中取出项目并处理它们,一次一个。具体如何管理这将取决于您的情况,但这并不是非常复杂。如果你能想出开始时需要做的所有工作,那就更容易了,这样线程就可以在发现队列为空时退出。

【讨论】:

  • 然而,与使用 ThreadPool 线程相比,这会增加相当多的启动时间。启动自己的线程并不快,而且可能没有必要。
  • @Reed:除非线程池已经预热,否则无论如何都会有这个成本......而且启动线程真的那么昂贵。在我的 netbook 上,启动和加入 1000 个线程只需不到 1 秒的时间。我不认为这是“相当长的启动时间”——无论如何,OP 不太可能真的需要 1000 个线程。我认为这是比使用线程池更简单的解决方案。
  • Jon,但是使用 thread.join,我只是阻塞了主线程,直到它完成。但是如果我想在特定线程完成时收到通知,以便我可以在主线程中做一些进一步的操作呢?
  • @Jon: 是的,但是如果你产生 1000 个线程,并且所有 1000 个线程都在工作,你就会使系统负担过重,并导致非常讨厌的抖动。 ThreadPool 限制运行线程的数量是有原因的(TPL 在 .NET 4 中也非常适合这一点——因为它会根据工作在运行时调整总线程数)。 OP 特别说他想限制运行线程的总数,对我来说,这表明他可能会产生很多工作项......
  • 实际上,这正是我希望线程在操作完成后通知主线程的原因。例如,如果我有 1000 个工作单元......并且正在使用 10 个线程来处理工作。因此,在一个线程完成每个工作项后,我想产生另一个线程来获取下一个待处理的工作项。这样我想限制同时运行 10 个线程。
【解决方案2】:

这在控制台应用程序中不起作用,因为SynchronizationContext.Current 永远不会被初始化。当您使用 GUI 应用程序时,它由 Windows 窗体或 WPF 为您初始化。

话虽如此,没有理由这样做。只需使用ThreadPool.QueueUserWorkItem 和一个重置事件(ManualResetEventAutoResetEvent)来捕获完成状态,并阻塞您的主线程。


编辑:

看了一些OP cmets后,我想我会添加这个。

在我看来,“最好的”替代方案是获取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 时)。

【讨论】:

    【解决方案3】:

    你说得对,BackgroundWorker 在这里不起作用。它确实设计用于与 GUI 环境(WinForms 和 WPF 类似)一起工作,其中主线程一直忙于检查/执行消息泵(处理所有 Windows 事件,如 Click 和 Resize,以及来自 backgroundWorker 的事件)。 如果没有事件队列,主线程将耗尽。如果没有事件队列,BackgroundWorker 也无法在主线程上调用回调。

    确实,在控制台上执行多线程要困难十几倍,因为您没有消息泵来解决两个大问题:如何保持主线程处于活动状态以及如何通知主线程 UI 上所需的更改。基于事件的编程被证明是多线程的理想学习场所。

    有一些解决方案:

    使用消息泵扩展您的控制台应用程序。因为它只是一个线程共享的委托集合(其他线程只添加东西)和一个while循环,所以它非常简单: C# Console App + Event Handling

    线程池和线程类。它们与 BackgroundWorker 一样长(自 .NET 2.0 起),似乎旨在解决 Console 的问题。你仍然需要用一个循环来阻塞你的主线程。 ThreadPool 还会将线程数限制为适合真机的东西。您应该避免固定的线程限制,而是依靠操作系统的线程池功能来处理“一次应该运行多少线程?”的问题。 例如,固定的 5 线程数可能非常适合您的 6 核开发机器。但对于单核台式机来说,它太多了。在拥有 16 到 64 个内核且有 GiB 的 RAM 空闲的服务器上,这将是一个毫无意义的瓶颈。

    如果您有 .NET 4.0 或更高版本,新添加的任务并行可能值得一看。 http://msdn.microsoft.com/en-us/library/dd537609.aspx 它内置了 ThreadPooling、有限的优先级能力以及可以轻松链接和加入多个任务(Thread 可以做的一切,Task 可以做的事情)。 使用 Task 还可以使用 async 和 await 关键字: http://msdn.microsoft.com/en-us/library/hh191443.aspx 同样,您不会绕过在控制台应用程序中阻塞主线程。

    【讨论】:

    • 实际上,事件非常易于使用,并且是一种保持控制台程序执行直到特定事件发生的高效方法。
    【解决方案4】:

    好吧,很抱歉 necro-answer,但在 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 月,所以我认为这已经工作了一段时间?)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-20
      • 1970-01-01
      • 2011-03-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多