Обходной путь для WaitHandle.Ограничение дескриптора WaitAll 64?
мое приложение порождает множество различных небольших рабочих потоков через 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();
8 ответов
создайте переменную, которая отслеживает количество выполняемых задач:
int numberOfTasks = 100;
создать сигнал:
ManualResetEvent signal = new ManualResetEvent(false);
уменьшить количество задач, когда задача завершена:
if (Interlocked.Decrement(ref numberOftasks) == 0)
{
если нет задачи, оставшейся, установите сигнал:
signal.Set();
}
тем временем, где-то еще, дождитесь сигнала, который будет установлен:
signal.WaitOne();
начиная с .NET 4.0, у вас есть еще два варианта (и IMO, cleaner), доступных для вас.
во-первых, использовать 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();
}
тем не менее, есть еще более надежное решение, и это использовать 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.Factory.StartNew(() => {
// Do work.
}
// No signalling, no anything.
).ToArray();
// Wait on all the tasks.
Task.WaitAll(tasks);
С помощью Task
класс и вызов WaitAll
намного чище, ИМО, поскольку вы плетете меньше примитивов по всему коду (обратите внимание, нет дескрипторов ожидания); вам не нужно настраивать счетчик, обрабатывать приращение/уменьшение, вы просто настраиваете свои задачи, а затем ждете их. Это позволяет коду быть более выразительным в что что вы хотите сделать, а не примитивы как (по крайней мере, с точки зрения управления распараллеливания его).
.NET 4.5 предлагает еще больше опций, вы можете упростить генерацию последовательности Task
экземпляры, вызывая статический Run
метод 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 (это 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>
by по умолчанию обрабатывается один элемент за раз, поэтому, если вы хотите, чтобы он обрабатывал несколько действий одновременно, вы должны установить количество одновременных элементов, которые вы хотите обработать в конструкторе, передав 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 становится много более привлекательный вариант, в том, что он обрабатывает параллелизм через все в задачи связаны между собой, и это позволяет быть очень конкретным о просто как параллельно вы хотите, чтобы каждая часть была, сохраняя при этом надлежащее разделение проблем для каждого блока.
ваш обходной путь не является правильным. Причина в том, что Set
и WaitOne
может участвовать в гонке, если последний рабочий элемент вызывает threadCount
к нулю до поток очереди должен был случайно встать в очередь все рабочих элементов. Исправление прост. Рассматривайте поток очереди как рабочий элемент. Инициализировать 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();
добавление к ответу 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();
}
}
добавление к ответу 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;
}
}
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);
}
}
Я решил это, просто разбив количество событий на страницы, чтобы ждать без потери производительности, и он отлично работает в производственной среде. Следующий код:
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();
}
Windows XP SP3 поддерживает максимум два WaitHandles. В случаях более 2 WaitHandles применение преждевременно прекращается.