【问题标题】:Method using EventWaitHandles returning earlier than expected使用 EventWaitHandles 的方法早于预期返回
【发布时间】: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


【解决方案1】:

这是一个修改后的解决方案,它使用CountdownEvent 信号来避免使用remaining 整数并避免使用不可靠的AutoResetEvent onComplete 轮询它所涉及的繁忙等待:

public static void PerformActionsInParallel<T>(IEnumerable<T> elements, Action<T> action)
{
    int threads = MaxConcurrent ?? DefaultMaxConcurrentRequests;
    // Ensure elements is only enumerated once.
    elements = elements as T[] ?? elements.ToArray();
    // 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
    CountdownEvent remaining = new CountdownEvent(elements.Count());

    foreach (T element in elements)
    {
        limit.WaitOne();
        new Thread(() =>
        {
            try
            {
                action(element);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error performing concurrent action: " + ex);
            }
            finally
            {
                remaining.Signal();
                limit.Release();
            }
        }).Start();
    }
    // Wait for all requests to complete
    remaining.Wait();
}

正在进行测试,看看是否能解决问题。

【讨论】:

    猜你喜欢
    • 2017-10-21
    • 1970-01-01
    • 2011-06-08
    • 1970-01-01
    • 1970-01-01
    • 2019-03-11
    • 1970-01-01
    • 2020-02-08
    相关资源
    最近更新 更多