【问题标题】:Spawn Multiple Threads for work then wait until all finished生成多个线程进行工作,然后等到全部完成
【发布时间】:2010-03-27 10:33:18
【问题描述】:

只是想要一些关于多线程任务的“最佳实践”的建议。

例如,我们有一个 C# 应用程序,它在启动时从数据库中的各种“类型”表中读取数据,并将信息存储在我们在应用程序周围传递的集合中。这可以防止我们在每次需要这些信息时访问数据库。

此时应用程序正在同步从 10 个表中读取数据。我真的很想让应用程序从不同线程中的每个表中读取,所有这些都并行运行。应用程序将等待所有线程完成,然后再继续启动应用程序。

我已经研究过 BackGroundWorker,但只是想要一些关于完成上述工作的建议。

  1. 为了加快我们的应用程序的启动时间,该方法听起来合乎逻辑吗
  2. 我们如何最好地处理所有线程,记住每个线程的工作是相互独立的,我们只需要等待所有线程完成后再继续。

我期待一些答案

【问题讨论】:

  • 看看并行任务库,这确实会为您简化事情。
  • 您指的是任务并行库 (TPL) 吗?我会调查的
  • TPL 非常适合这一点,但至少需要 .NET 3.5sp1(如果您安装 Rx)或 .NET 4 RC(内置框架)才能使用....您建议您需要.NET 2 解决方案。
  • 出于好奇,您是否尝试过这些解决方案并发现性能优势?如果您正在读取的基础数据来自同一来源,那么使读取并行可能根本没有任何好处,因为无论如何它们最终都会相互等待资源访问。所发生的只是额外的上下文切换开销。多线程实际上只对性能有益,如果 a) 你实际上有多个内核 b) 你被计算控制而不访问共享资源
  • 如果这个问题有一个被接受的答案,那就太好了,尤其是有很多不同的用户为此付出了时间

标签: c# multithreading backgroundworker


【解决方案1】:

我的偏好是通过单个 WaitHandle 来处理这个问题,并使用 Interlocked 来避免锁定在计数器上:

class Program
{
    static void Main(string[] args)
    {
        int numThreads = 10;
        ManualResetEvent resetEvent = new ManualResetEvent(false);
        int toProcess = numThreads;

        // Start workers.
        for (int i = 0; i < numThreads; i++)
        {
            new Thread(delegate()
            {
                Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                // If we're the last thread, signal
                if (Interlocked.Decrement(ref toProcess) == 0)
                    resetEvent.Set();
            }).Start();
        }

        // Wait for workers.
        resetEvent.WaitOne();
        Console.WriteLine("Finished.");
    }
}

这很好用,并且可以扩展到任意数量的线程处理,而不会引入锁定。

【讨论】:

  • 你说的非常对。所有其他解决方案都可能会虚假地唤醒等待线程或至少将其放在就绪队列中。有时必须避免这种情况——如果不是,至少应该避免。我要删。 :P
  • @andras: (顺便说一句 - 对另一个答案的“处理结束时锁定”评论 - 即使锁定在那里也会产生延迟事情的“排队”效果。有 10 个线程,这不是问题,但是如果您尝试与数百人同步,或者如果您正在重新安排时间,它实际上会减慢速度)
  • @Reed:如果我们真的在吹毛求疵,:) 可以说Interlocked 也会产生“排队”效应。 (没有两个 CPU 可以在它们的高速缓存中同时保存相同地址的内容以供写入。它们必须使高速缓存行无效并通过主存储器或高速缓存间通信传递新内容。这会创建一个如果您从不同的 CPU 更新相同的内存位置,则会产生“排队”效应 - 必须对写入进行序列化。)
  • @andras:更多的是他们会失效,并刷新缓存行——但它实际上并没有锁定,而是创建了一个队列。锁使缓存行无效,但也迫使其他线程等待(可以说,这比缓存行未命中更糟糕......)
  • @Reed:最后,我认为你不能同时将两个不同的人放在同一个地方。在一个线程上互锁将强制其他线程上的相同互锁操作等待......(不管它们是按 FIFO、LIFO 还是其他任意顺序提供服务,它们都必须等待。你不妨说他们必须在队列中等待 - 这当然不精确,但可以很容易地可视化。当然是一种“排队”效应,如果你的意思是它导致按顺序处理内存存储,一个-一个..):P
【解决方案2】:

我喜欢@Reed 的解决方案。在 .NET 4.0 中实现相同功能的另一种方法是使用 CountdownEvent

class Program
{
    static void Main(string[] args)
    {
        var numThreads = 10;
        var countdownEvent = new CountdownEvent(numThreads);

        // Start workers.
        for (var i = 0; i < numThreads; i++)
        {
            new Thread(delegate()
            {
                Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                // Signal the CountdownEvent.
                countdownEvent.Signal();
            }).Start();
        }

        // Wait for workers.
        countdownEvent.Wait();
        Console.WriteLine("Finished.");
    }
}

【讨论】:

  • 这很好,唯一的问题是在进入循环之前你必须知道有多少线程......所以如果你的循环中有 continues 的逻辑,这是没有那么有用,但如果不需要,这感觉像是更清洁的方式。
  • 这在玩具示例中没有任何区别,但总的来说,处理 countdownEvent 可能是个好主意 -- using (var countdownEvent = new CountdownEvent(numThreads)) { ... }?
【解决方案3】:

如果您有超过 64 个 STA 线程的等待句柄,如 Mark 所说。你可以用你的线程创建一个列表,然后等待所有线程在第二个循环中完成。

//check that all threads have completed.
foreach (Thread thread in threadList)
{
     thread.Join();

}  

【讨论】:

    【解决方案4】:

    如果您不在 .NET 4.0 上,那么您可以使用 ListManualResetEvent>,每个线程一个,Wait 则它们是 Set。要在多个线程上等待,您可以考虑使用WaitAll,但要注意 64 个等待句柄的限制。如果您需要的不止这些,您可以遍历它们并单独等待每个。

    如果您想要更快的启动体验,您可能不需要在启动期间等待所有数据被读取。只需显示 GUI,任何丢失的信息都可以用某种“正在更新...”图标或类似图标显示为灰色。当信息进入时,只需触发一个事件来更新 GUI。甚至在读入所有表中的所有数据之前,用户就可以开始执行许多操作。

    【讨论】:

    • ManualResetEvent 是我最喜欢的同步工具!
    • Mark:这可以通过单个 ManualResetEvent 和 Interlocked 类来完成。有关详细信息,请参阅下面的回复。
    【解决方案5】:

    如果您喜欢冒险,可以使用 C# 4.0 和任务并行库:

    Parallel.ForEach(jobList, curJob => {
      curJob.Process()
    });
    

    【讨论】:

    • mmmmm thx 但 C# 4.0 似乎在商业应用程序曲线的右端。可能必须想出一个 C# 2.0 解决方案。
    【解决方案6】:

    这是等待多个并行操作的两种模式。诀窍是您必须将主线程也视为并行操作之一。否则,工作线程中的完成信号与主线程等待该信号之间存在微妙的竞争条件。

    int threadCount = 1;
    ManualResetEvent finished = new ManualResetEvent(false);
    for (int i = 0; i < NUM_WORK_ITEMS; i++)
    {
      Interlocked.Increment(ref threadCount); 
      ThreadPool.QueueUserWorkItem(delegate 
      { 
          try 
          { 
               // do work 
          } 
          finally 
          { 
              if (Interlocked.Decrement(ref threadCount) == 0) finished.Set();
          } 
      }); 
    }
    if (Interlocked.Decrement(ref threadCount) == 0) finished.Set();
    finished.WaitOne(); 
    

    作为个人喜好,我喜欢使用 CountdownEvent 类为我进行计数,这在 .NET 4.0 中可用。

    var finished = new CountdownEvent(1);
    for (int i = 0; i < NUM_WORK_ITEMS; i++)
    {
      finished.AddCount();
      ThreadPool.QueueUserWorkItem(delegate 
      { 
          try 
          { 
               // do work 
          } 
          finally 
          { 
            finished.Signal();
          } 
      }); 
    }
    finished.Signal();
    finished.Wait(); 
    

    上面的示例使用ThreadPool,但您可以将其替换为您喜欢的任何线程机制。

    【讨论】:

      【解决方案7】:

      只是为了好玩,@Reed 用 Monitor 做了什么。 :P

      class Program
      {
          static void Main(string[] args)
          {
              int numThreads = 10;
              int toProcess = numThreads;
              object syncRoot = new object();
      
              // Start workers.
              for (int i = 0; i < numThreads; i++)
              {
                  new Thread(delegate()
                  {
                      Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                      // If we're the last thread, signal
                      if (Interlocked.Decrement(ref toProcess) == 0)
                      {
                          lock (syncRoot)
                          {
                              Monitor.Pulse(syncRoot);
                          }
                      }
                  }).Start();
              }
      
              // Wait for workers.
              lock (syncRoot)
              {
                  if (toProcess > 0)
                  {
                      Monitor.Wait(syncRoot);
                  }
              }
      
              Console.WriteLine("Finished.");
          }
      }
      

      【讨论】:

      • @andras:我使用单个 ManualResetEvent 对此进行了另一次修改 - 它更好,因为它完全消除了处理中锁定的需要,因此它可以更好地扩展到更大的工人数量。信号量选项是类似的,虽然我个人认为比我下面的答案更复杂。
      • @Reed:我对信号量解决方案非常不满意。 (我什至曾一度将其从答案中删除。)即使是基于监视器的解决方案似乎也好得多。您的解决方案在各方面都更好。 (......性能方面,它是王者......):)
      【解决方案8】:

      TPL 的另一种可能性,假设 jobs 是要处理的项目集合或要运行的子线程:

      Task.WaitAll(jobs
          .Select(job => TaskFactory.StartNew(() => /*run job*/))
          .ToArray());
      

      【讨论】:

        【解决方案9】:

        假设数据库读取器线程一完成就返回,您可以简单地从启动线程依次在所有十个线程上调用 Thread.Join。

        【讨论】:

          【解决方案10】:

          如果您使用的是 .NET 3.5 或更低版本,则可以使用 AsyncResult 或 BackgroundWorker 数组并计算返回的线程数(只是不要忘记使用联锁操作减少计数器)(请参阅 http://www.albahari.com/threading/ 作为参考)。

          如果您使用的是 .NET 4.0,并行 for 是最简单的方法。

          【讨论】:

          • @Kiquenet 我目前可以访问的任何内容都没有。我不确定这是要求提供完整代码示例的正确网站。
          【解决方案11】:

          我喜欢使用的更简单的方法:

              private int ThreadsCount = 100; //initialize threads count
              private void button1_Click(object sender, EventArgs e)
              {   
                  for (int i = 0; i < ThreadsCount; i++)
                  {
                      Thread t = new Thread(new ThreadStart(myMethod));
                      t.IsBackground = true;
                      t.Start(); 
                  } 
              }
          
              private void myMethod()
              {
                  //everytime a thread finishes executing decrease the threads count
                  ThreadsCount = ThreadsCount - 1;
          
                  if (ThreadsCount < 1)
                  {
                      //if all threads finished executing do whatever you wanna do here..
                      MessageBox.Show("Finished Executing all threads!!!");
                  }
              }
          

          【讨论】:

            【解决方案12】:

            发帖可能是为了帮助其他人,花了很多时间寻找像我想出的解决方案。所以我采取了一些不同的方法。当线程启动和停止时,我正在关闭许多线程并增加一个计数器并减少一个计数器。然后在 main 方法中,我想暂停并等待线程完成。

            while (threadCounter > 0)
            {
                Thread.Sleep(500); //Make it pause for half second so that we don’t spin the cpu out of control.
            }
            

            记录在我的博客上。 http://www.adamthings.com/post/2012/07/11/ensure-threads-have-finished-before-method-continues-in-c/

            【讨论】:

            • 我无法让 Thread.Join() 阻止 PageLoad 完成,而作为一个简单的计数器,我可以循环遍历直到所有线程完成。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2016-07-14
            • 2017-10-07
            • 1970-01-01
            • 2021-11-12
            相关资源
            最近更新 更多