【发布时间】:2015-11-16 17:54:48
【问题描述】:
我创建了一个简单的方法,它应该获取一个包含 100 个项目的列表,并异步处理它们(一次最多 MAX_CONCURRENT 个元素),并且仅在处理完所有元素后返回:
/// <summary>Generic method to perform an action or set of actions
/// in parallel on each item in a collection of items, returning
/// only when all actions have been completed.</summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="elements">A collection of elements, each of which to
/// perform the action on.</param>
/// <param name="action">The action to perform on each element. The
/// action should of course be thread safe.</param>
/// <param name="MAX_CONCURRENT">The maximum number of concurrent actions.</param>
public static void PerformActionsInParallel<T>(IEnumerable<T> elements, Action<T> action)
{
// Semaphore limiting the number of parallel requests
Semaphore limit = new Semaphore(MAX_CONCURRENT, MAX_CONCURRENT);
// Count of the number of remaining threads to be completed
int remaining = 0;
// Signal to notify the main thread when a worker is done
AutoResetEvent onComplete = new AutoResetEvent(false);
foreach (T element in elements)
{
Interlocked.Increment(ref remaining);
limit.WaitOne();
new Thread(() =>
{
try
{
action(element);
}
catch (Exception ex)
{
Console.WriteLine("Error performing concurrent action: " + ex);
}
finally
{
Interlocked.Decrement(ref remaining);
limit.Release();
onComplete.Set();
}
}).Start();
}
// Wait for all requests to complete
while (remaining > 0)
onComplete.WaitOne(10); // Slightly better than Thread.Sleep(10)
}
/* We include a timeout on the `WaitOne()` before checking `remaining` again
* to protect against the rare case where the last outstanding thread
* decrements 'remaining' and then signals completion *between* the main thread
* checking 'remaining' and waiting for the next completion signal, which would
* otherwise result in the main thread missing the last signal and locking forever. */
大多数时候,这段代码的行为完全符合预期,但在极少数情况下,我发现该方法在列表中的每个元素完成处理之前返回(即跳出最后的 while 循环)。当只剩下几个元素时,它似乎总是会发生 - 例如我将处理 97 个元素,然后方法返回,然后完成 98-100 个元素。
我做错了什么,可能导致 remaining 在所有元素都被实际处理之前达到 0?
【问题讨论】:
-
形式上是错误的,int 没有以线程安全的方式使用。由于 int 没有被声明为 volatile,while 循环也很有可能永远不会完成。改用 CountDownEvent 类,去掉 onComplete。
-
请注意,虽然我同意 Hans 的观察和建议,但您可以通过使用
Volatile.Read()访问末尾的值来解决不稳定的问题(这就是缺少的地方线程安全是一个问题)。我仍然不建议这样做。轮询这样的变量是浪费且不必要的。使用有效的适当同步机制。坦率地说,我不清楚为什么要轮询变量。只需让最后一个线程(即,将remaining减为 0 的线程)设置事件,然后等待事件。或者使用为此而生的CountdownEvent -
另请注意,您似乎在重塑
Parallel.ForEach()。如果这是一个学术练习,那很好……但如果不是,请考虑使用内置功能而不是再次编写它们。 :) -
@HansPassant 什么不是线程安全的?我使用
Interlocked.Decrement,我认为读取 32 位 int 是原子的? -
@PeterDuniho 我试过了,但我不能只剩下递减为 0 的线程,因为如果 for 循环还没有执行它的第二个循环,
remaining仍将设置为 1当它减回到 0 并且complete将被设置得太早,
标签: c# multithreading thread-safety locking