53

我的应用程序产生了大量不同的小型工作线程,我通过多个实例ThreadPool.QueueUserWorkItem跟踪这些线程。ManualResetEvent我使用该WaitHandle.WaitAll方法阻止我的应用程序关闭,直到这些线程完成。

但是,我以前从未遇到过任何问题,因为我的应用程序正在承受更多负载,即创建更多线程,我现在开始遇到此异常:

WaitHandles must be less than or equal to 64 - missing documentation

什么是最好的替代解决方案?

代码片段

List<AutoResetEvent> events = new List<AutoResetEvent>();

// multiple instances of...
var evt = new AutoResetEvent(false);
events.Add(evt);
ThreadPool.QueueUserWorkItem(delegate
{
    // do work
    evt.Set();
});

...
WaitHandle.WaitAll(events.ToArray());

解决方法

int threadCount = 0;
ManualResetEvent finished = new ManualResetEvent(false);

...
Interlocked.Increment(ref threadCount);
ThreadPool.QueueUserWorkItem(delegate
{
    try
    {
         // do work
    }
    finally
    {
        if (Interlocked.Decrement(ref threadCount) == 0)
        {
             finished.Set();
        }
    }
});

...
finished.WaitOne();
4

8 回答 8

47

创建一个跟踪正在运行的任务数量的变量:

int numberOfTasks = 100;

创建一个信号:

ManualResetEvent signal = new ManualResetEvent(false);

每当任务完成时减少任务数:

if (Interlocked.Decrement(ref numberOftasks) == 0)
{

如果没有剩余任务,设置信号:

    signal.Set();
}

同时,在其他地方等待信号被设置:

signal.WaitOne();
于 2010-04-23T23:35:24.163 回答
43

从 .NET 4.0 开始,您可以使用另外两个(以及 IMO,更清洁)选项。

首先是使用CountdownEvent。它避免了必须自己处理递增和递减的需要:

int tasks = <however many tasks you're performing>;

// Dispose when done.
using (var e = new CountdownEvent(tasks))
{
    // Queue work.
    ThreadPool.QueueUserWorkItem(() => {
        // Do work
        ...

        // Signal when done.
        e.Signal();
    });

    // Wait till the countdown reaches zero.
    e.Wait();
}

但是,还有一个更强大的解决方案,那就是使用Taskclass,如下所示:

// The source of your work items, create a sequence of Task instances.
Task[] tasks = Enumerable.Range(0, 100).Select(i =>
    // Create task here.
    Task.Factory.StartNew(() => {
        // Do work.
    }

    // No signalling, no anything.
).ToArray();

// Wait on all the tasks.
Task.WaitAll(tasks);

使用Task类和调用WaitAll更干净,IMO,因为您在整个代码中编织的线程原语更少(注意,没有等待句柄);您不必设置计数器,处理递增/递减,您只需设置您的任务,然后等待它们。这让代码更能表达你想要做什么而不是如何做的原语至少在管理它的并行化方面)。

.NET 4.5 提供了更多选项,您可以通过调用类上的静态方法Task来简化实例序列的生成:RunTask

// The source of your work items, create a sequence of Task instances.
Task[] tasks = Enumerable.Range(0, 100).Select(i =>
    // Create task here.
    Task.Run(() => {
        // Do work.
    })

    // No signalling, no anything.
).ToArray();

// Wait on all the tasks.
Tasks.WaitAll(tasks);

或者,您可以利用TPL DataFlow 库(它位于System命名空间中,因此它是官方的,即使它是从 NuGet 下载的,如 Entity Framework)并使用ActionBlock<TInput>,如下所示:

// Create the action block.  Since there's not a non-generic
// version, make it object, and pass null to signal, or
// make T the type that takes the input to the action
// and pass that.
var actionBlock = new ActionBlock<object>(o => {
    // Do work.
});

// Post 100 times.
foreach (int i in Enumerable.Range(0, 100)) actionBlock.Post(null);

// Signal complete, this doesn't actually stop
// the block, but says that everything is done when the currently
// posted items are completed.
actionBlock.Complete();

// Wait for everything to complete, the Completion property
// exposes a Task which can be waited on.
actionBlock.Completion.Wait();

请注意,ActionBlock<TInput>默认情况下一次处理一项,因此如果您想让它一次处理多个操作,您必须通过传递ExecutionDataflowBlockOptions实例并设置MaxDegreeOfParallelism属性来设置要在构造函数中处理的并发项数:

var actionBlock = new ActionBlock<object>(o => {
    // Do work.
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 4 });

如果您的操作确实是线程安全的,那么您可以将MaxDegreeOfParallelsim属性设置为DataFlowBlockOptions.Unbounded

var actionBlock = new ActionBlock<object>(o => {
    // Do work.
}, new ExecutionDataflowBlockOptions { 
    MaxDegreeOfParallelism = DataFlowBlockOptions.Unbounded
});

关键是,您可以细粒度地控制希望选项的并行程度。

当然,如果您有一系列想要传递到您的ActionBlock<TInput>实例中的项目,那么您可以链接一个ISourceBlock<TOutput>实现来提供ActionBlock<TInput>,如下所示:

// The buffer block.
var buffer = new BufferBlock<int>();

// Create the action block.  Since there's not a non-generic
// version, make it object, and pass null to signal, or
// make T the type that takes the input to the action
// and pass that.
var actionBlock = new ActionBlock<int>(o => {
    // Do work.
});

// Link the action block to the buffer block.
// NOTE: An IDisposable is returned here, you might want to dispose
// of it, although not totally necessary if everything works, but
// still, good housekeeping.
using (link = buffer.LinkTo(actionBlock, 
    // Want to propagate completion state to the action block.
    new DataflowLinkOptions {
        PropagateCompletion = true,
    },
    // Can filter on items flowing through if you want.
    i => true)
{ 
    // Post 100 times to the *buffer*
    foreach (int i in Enumerable.Range(0, 100)) buffer.Post(i);

    // Signal complete, this doesn't actually stop
    // the block, but says that everything is done when the currently
    // posted items are completed.
    actionBlock.Complete();

    // Wait for everything to complete, the Completion property
    // exposes a Task which can be waited on.
    actionBlock.Completion.Wait();
}

根据您需要做什么,TPL Dataflow 库成为一个更具吸引力的选择,因为它可以处理链接在一起的所有任务的并发性,并且它允许您非常具体地了解希望每个部分的并行度,同时为每个块保持适当的关注点分离。

于 2012-10-14T17:17:28.890 回答
18

您的解决方法不正确。原因是如果最后一个工作项导致在排队线程必须有机会将所有工作项排队之前变为零,则SetandWaitOne可能会竞争。修复很简单。将排队线程视为工作项本身。初始化为 1 并在排队完成时进行减量并发出信号。threadCountthreadCount

int threadCount = 1;
ManualResetEvent finished = new ManualResetEvent(false);
...
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班级为我计算。

var finished = new CountdownEvent(1);
...
finished.AddCount();
ThreadPool.QueueUserWorkItem(delegate 
{ 
    try 
    { 
         // do work 
    } 
    finally 
    { 
      finished.Signal();
    } 
}); 
... 
finished.Signal();
finished.Wait(); 
于 2010-10-01T17:31:34.150 回答
6

添加到 dtb 的答案中,您可以将其包装成一个不错的简单类。

public class Countdown : IDisposable
{
    private readonly ManualResetEvent done;
    private readonly int total;
    private long current;

    public Countdown(int total)
    {
        this.total = total;
        current = total;
        done = new ManualResetEvent(false);
    }

    public void Signal()
    {
        if (Interlocked.Decrement(ref current) == 0)
        {
            done.Set();
        }
    }

    public void Wait()
    {
        done.WaitOne();
    }

    public void Dispose()
    {
        ((IDisposable)done).Dispose();
    }
}
于 2010-04-24T01:11:17.040 回答
0

当我们想要回调时添加到 dtb 的答案。

using System;
using System.Runtime.Remoting.Messaging;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        Main m = new Main();
        m.TestMRE();
        Console.ReadKey();

    }
}

class Main
{
    CalHandler handler = new CalHandler();
    int numberofTasks =0;
    public void TestMRE()
    {

        for (int j = 0; j <= 3; j++)
        {
            Console.WriteLine("Outer Loop is :" + j.ToString());
            ManualResetEvent signal = new ManualResetEvent(false);
            numberofTasks = 4;
            for (int i = 0; i <= 3; i++)
            {
                CalHandler.count caller = new CalHandler.count(handler.messageHandler);
                caller.BeginInvoke(i, new AsyncCallback(NumberCallback),signal);
            }
            signal.WaitOne();
        }

    }

    private void NumberCallback(IAsyncResult result)
    {
        AsyncResult asyncResult = (AsyncResult)result;

        CalHandler.count caller = (CalHandler.count)asyncResult.AsyncDelegate;

        int num = caller.EndInvoke(asyncResult);

        Console.WriteLine("Number is :"+ num.ToString());

        ManualResetEvent mre = (ManualResetEvent)asyncResult.AsyncState;
        if (Interlocked.Decrement(ref numberofTasks) == 0)
        {
            mre.Set();
        }
    }

}
public class CalHandler
{
    public delegate int count(int number);

    public int messageHandler ( int number )
    {
        return number;
    }

}
于 2016-02-24T22:14:39.673 回答
0
protected void WaitAllExt(WaitHandle[] waitHandles)
{
    //workaround for limitation of WaitHandle.WaitAll by <=64 wait handles
    const int waitAllArrayLimit = 64;
    var prevEndInd = -1;
    while (prevEndInd < waitHandles.Length - 1)
    {
        var stInd = prevEndInd + 1;
        var eInd = stInd + waitAllArrayLimit - 1;
        if (eInd > waitHandles.Length - 1)
        {
            eInd = waitHandles.Length - 1;
        }
        prevEndInd = eInd;

        //do wait
        var whSubarray = waitHandles.Skip(stInd).Take(eInd - stInd + 1).ToArray();
        WaitHandle.WaitAll(whSubarray);
    }

}
于 2016-12-12T12:50:55.657 回答
0

我确实通过简单地对要等待的事件数量进行分页来解决它,而不会损失太多性能,并且它在生产环境中运行良好。遵循代码:

        var events = new List<ManualResetEvent>();

        // code omited

        var newEvent = new ManualResetEvent(false);
        events.Add(newEvent);
        ThreadPool.QueueUserWorkItem(c => {

            //thread code
            newEvent.Set();
        });

        // code omited

        var wait = true;
        while (wait)
        {
            WaitHandle.WaitAll(events.Take(60).ToArray());
            events.RemoveRange(0, events.Count > 59 ? 60 : events.Count);
            wait = events.Any();

        }
于 2017-08-16T20:13:25.617 回答
0

这是另一个解决方案。这里的“事件”是ManualResetEvent的列表。列表的大小可以大于 64 (MAX_EVENTS_NO)。

int len = events.Count;
if (len <= MAX_EVENTS_NO)
    {
        WaitHandle.WaitAll(events.ToArray());
    } else {
        int start = 0;
        int num = MAX_EVENTS_NO;
        while (true)
            {
                if(start + num > len)
                {
                   num = len - start;
                }
                List<ManualResetEvent> sublist = events.GetRange(start, num);
                WaitHandle.WaitAll(sublist.ToArray());
                start += num;
                if (start >= len)
                   break;
           }
   }

于 2019-09-26T15:08:22.230 回答