创建多个线程并等待它们全部完成

89

我如何创建多个线程并等待它们全部完成?

8个回答

154

这取决于你使用的.NET Framework版本。.NET 4.0使用任务(Tasks)使线程管理变得更加容易:

class Program
{
    static void Main(string[] args)
    {
        Task task1 = Task.Factory.StartNew(() => doStuff());
        Task task2 = Task.Factory.StartNew(() => doStuff());
        Task task3 = Task.Factory.StartNew(() => doStuff());

        Task.WaitAll(task1, task2, task3);
                Console.WriteLine("All threads complete");
    }

    static void doStuff()
    {
        //do stuff here
    }
}

在之前的.NET版本中,您可以使用BackgroundWorker对象、使用ThreadPool.QueueUserWorkItem(),或手动创建线程并使用Thread.Join()等待它们完成:

static void Main(string[] args)
{
    Thread t1 = new Thread(doStuff);
    t1.Start();

    Thread t2 = new Thread(doStuff);
    t2.Start();

    Thread t3 = new Thread(doStuff);
    t3.Start();

    t1.Join();
    t2.Join();
    t3.Join();

    Console.WriteLine("All threads complete");
}

7
任务 API 是目前最简洁的解决方案。 - JefClaes
1
需要注意一些限制 - 如果您需要一个线程具有特定的优先级,则不能使用任务。技术上讲,您可以这样做,但是在任务内更改线程优先级是一个坏主意,因为该线程属于线程池,所以您的自定义优先级可能会影响其他代码。 - JustAMartin

40

我认为你需要使用WaitHandler.WaitAll。这里有一个例子:

public static void Main(string[] args)
{
    int numOfThreads = 10;
    WaitHandle[] waitHandles = new WaitHandle[numOfThreads];

    for (int i = 0; i < numOfThreads; i++)
    {
        var j = i;
        // Or you can use AutoResetEvent/ManualResetEvent
        var handle = new EventWaitHandle(false, EventResetMode.ManualReset);
        var thread = new Thread(() =>
                                {
                                    Thread.Sleep(j * 1000);
                                    Console.WriteLine("Thread{0} exits", j);
                                    handle.Set();
                                });
        waitHandles[j] = handle;
        thread.Start();
    }
    WaitHandle.WaitAll(waitHandles);
    Console.WriteLine("Main thread exits");
    Console.Read();
}

FCL有一些更方便的函数。

(1) Task.WaitAll,以及它的重载函数,用于在并行执行一些任务时(且返回值为空)。

var tasks = new[]
{
    Task.Factory.StartNew(() => DoSomething1()),
    Task.Factory.StartNew(() => DoSomething2()),
    Task.Factory.StartNew(() => DoSomething3())
};
Task.WaitAll(tasks);

(2) Task.WhenAll 适用于想要执行一些带有返回值的任务。它会执行这些操作,并将结果放入一个数组中。它是线程安全的,因此您不需要使用线程安全容器并自己实现添加操作。

var tasks = new[]
{
    Task.Factory.StartNew(() => GetSomething1()),
    Task.Factory.StartNew(() => GetSomething2()),
    Task.Factory.StartNew(() => GetSomething3())
};
var things = Task.WhenAll(tasks);

@Kirk:我本来想现在添加一个例子,但是不得不去开会了。 - Cheng Chen
方法中'无返回值'和'有返回值'的良好解释! - SHEKHAR SHETE
你的线程代码正是我需要的,因为我想模拟大量请求,而不仅仅使用“更智能”的TPL方法提供给我的几个线程。 - madannes

9

我写了一个非常简单的扩展方法来等待集合中所有线程:

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

namespace Extensions {
    public static class ThreadExtension {
        public static void WaitAll (this IEnumerable<Thread> threads) {
            if (threads != null) {
                foreach (Thread thread in threads) {
                    thread.Join();
                }
            }
        }
    }
}

然后,您只需调用以下代码:
List<Thread> threads = new List<Thread>();
// Add your threads to this collection
threads.WaitAll();

我更愿意使用 ThreadHelpers.WaitAll(threadCollection).. 无论如何,这基本上是我在测试中使用的。实际代码中我很少需要“等待全部”。 - user2864740
需要解释一下。例如,它的运行原理是什么? - Peter Mortensen

5
在.NET 4.0中,您可以使用任务并行库(TPL)
在早期版本中,您可以在循环中创建Thread对象列表,对每个对象调用Start,然后再创建另一个循环并对每个对象调用Join

2
如果你在启动线程后立即调用 Join,那么你将等待它完成才能启动其他任何线程。你需要先启动 所有 线程,然后Join 所有线程。 - SLaks

5

如果您不想使用 Task 类(例如在 .NET 3.5 版本中),您可以启动所有线程,然后将它们添加到列表中,在 foreach 循环中调用 join 方法。

例如:

List<Thread> threads = new List<Thread>();

// Start threads
for (int i = 0; i < 10; i++) {
    int tmp = i; // Copy value for closure
    Thread t = new Thread(() => Console.WriteLine(tmp));
    t.Start();
    threads.Add(t);
}

// Join threads (wait threads)
foreach (Thread thread in threads) {
    thread.Join();
}

1

我不知道是否有更好的方法,但以下是我使用计数器和后台工作线程的方式。

private object _lock = new object();
private int _runningThreads = 0;

private int Counter{
    get{
        lock(_lock)
            return _runningThreads;
    }
    set{
        lock(_lock)
            _runningThreads = value;
    }
}

现在每当您创建一个工作线程时,请增加计数器:

var t = new BackgroundWorker();
// Add RunWorkerCompleted handler

// Start thread
Counter++;

在完成工作后,递减计数器:

private void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    Counter--;
}

现在您可以随时检查计数器,以查看是否有任何线程正在运行:
if(Couonter>0){
    // Some thread is yet to finish.
}

1
你不需要锁。实际上,由于你只从 UI 线程写入属性,所以什么都不需要。 - SLaks
2
我可能错了,但是我认为锁定正确;他正在一个事件处理程序中递减计数器,当每个工作人员完成时触发。 - Mark Avenius
2
@Mark:Completed事件总是在UI线程上触发。此外,锁定不会产生任何影响;Int32读写是原子的。如果存在线程问题,锁定将无法帮助;他需要调用“Interlocked.Increment”。 - SLaks
锁定对于计数器不起作用——因为在get/set之间,计数器是未受保护的。 - Greg Sansom
@Greg:确切地说,多线程编程很难。在每个成员上加锁远远不够。 - SLaks
显示剩余6条评论

0
在我的情况下,我无法使用Task.Run()Task.Factory.StartNew()在线程池上实例化我的对象。它们无法正确同步我的长时间运行的委托。
我需要异步运行这些委托,暂停主线程以等待它们的集体完成。由于我想在父线程中间等待集体完成,而不是在结尾处等待,所以Thread.Join()无法工作。
使用Task.Run()Task.Factory.StartNew()时,要么所有子线程相互阻塞,要么父线程不会被阻塞......我无法通过await语法的重新序列化来理解如何使用async委托。
以下是我使用线程而不是任务的解决方案:
using (EventWaitHandle wh = new EventWaitHandle(false, EventResetMode.ManualReset))
{
  int outdex = mediaServerMinConnections - 1;
  for (int i = 0; i < mediaServerMinConnections; i++)
  {
    new Thread(() =>
    {
      sshPool.Enqueue(new SshHandler());
      if (Interlocked.Decrement(ref outdex) < 1)
        wh.Set();
    }).Start();
  }
  wh.WaitOne();
}

0
大多数提出的答案都没有考虑到超时时间,这对于防止可能的死锁非常重要。下面是我的示例代码。(请注意,我主要是一个Win32开发者,这是我在那里的做法。)
//'arrRunningThreads' = List<Thread>

//Wait for all threads
const int knmsMaxWait = 3 * 1000;           //3 sec timeout
int nmsBeginTicks = Environment.TickCount;
foreach(Thread thrd in arrRunningThreads)
{
    //See time left
    int nmsElapsed = Environment.TickCount - nmsBeginTicks;
    int nmsRemain = knmsMaxWait - nmsElapsed;
    if(nmsRemain < 0)
        nmsRemain = 0;

    //Then wait for thread to exit
    if(!thrd.Join(nmsRemain))
    {
        //It didn't exit in time, terminate it
        thrd.Abort();

        //Issue a debugger warning
        Debug.Assert(false, "Terminated thread");
    }
}

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