我的应用程序通过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();
答案 0 :(得分:46)
创建一个跟踪正在运行的任务数量的变量:
int numberOfTasks = 100;
创建一个信号:
ManualResetEvent signal = new ManualResetEvent(false);
每当任务完成时减少任务数量:
if (Interlocked.Decrement(ref numberOftasks) == 0)
{
如果没有剩余任务,请设置信号:
signal.Set();
}
与此同时,在其他地方,等待设置信号:
signal.WaitOne();
答案 1 :(得分:37)
从.NET 4.0开始,您还可以使用两个(和IMO,更清洁)选项。
首先是使用CountdownEvent
class。它可以防止必须自己处理递增和递减:
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();
}
然而,有一个更强大的解决方案,那就是使用Task
class,如下所示:
// 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更清晰,因为你在整个代码中编织了较少的线程原语(通知,没有等待句柄);您不必设置计数器,处理递增/递减,您只需设置任务然后等待它们。这使得代码在您想要做的 what 中更具表现力,而不是如何的原语(至少在管理它的并行化方面)。
.NET 4.5提供了更多选项,您可以通过调用static Run
method on the Task
class来简化Task
个实例序列的生成:
// 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 library(它位于System
命名空间中,因此它是官方的,即使它是从NuGet下载,如实体框架)并使用{{3} },像这样:
// 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>
默认情况下一次处理一个项目,因此如果您希望一次处理多个操作,则必须设置要在构造函数中处理的并发项目数通过传递ActionBlock<TInput>
实例并设置ExecutionDataflowBlockOptions
:
var actionBlock = new ActionBlock<object>(o => {
// Do work.
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 4 });
如果您的操作确实是线程安全的,那么您可以将MaxDegreeOfParallelsim
属性设置为MaxDegreeOfParallelism
property:
var actionBlock = new ActionBlock<object>(o => {
// Do work.
}, new ExecutionDataflowBlockOptions {
MaxDegreeOfParallelism = DataFlowBlockOptions.Unbounded
});
关键在于,您可以对 并行方式进行细致的控制。
当然,如果您要将一系列项目传递到ActionBlock<TInput>
实例,那么您可以将DataFlowBlockOptions.Unbounded
实施链接到ActionBlock<TInput>
,如下所示:< / p>
// 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库成为很多更具吸引力的选项,因为它处理所有链接在一起的任务的并发性,并且它允许您非常具体地了解只是您希望每个部分的平行程度,同时保持每个块的关注点的正确分离。
答案 2 :(得分:17)
您的解决方法不正确。原因是Set
和WaitOne
可能会因为最后一个工作项导致threadCount
在排队线程必须有机会之前归零而竞争queue 所有工作项。修复很简单。将排队线程视为工作项本身。将threadCount
初始化为1,并在排队完成时进行减量并发出信号。
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();
答案 3 :(得分: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();
}
}
答案 4 :(得分: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;
}
}
答案 5 :(得分: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);
}
}
答案 6 :(得分: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();
}
答案 7 :(得分: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;
}
}
答案 8 :(得分:-2)
Windows XP SP3支持最多两个WaitHandles。对于超过2个WaitHandles应用程序过早终止的情况。