【问题标题】:Deadlock Risk in Nested Parallel For嵌套并行中的死锁风险
【发布时间】:2012-07-01 11:52:45
【问题描述】:

使用 ThreadPool 实现以下嵌套异步循环的幼稚实现:

ThreadPool.SetMaxThreads(10, 10);
CountdownEvent icnt = new CountdownEvent(1);
for (int i = 0; i < 50; i++)
{
    icnt.AddCount();
    ThreadPool.QueueUserWorkItem((inum) =>
    {
        Console.WriteLine("i" + inum + " scheduled...");
        Thread.Sleep(10000);  // simulated i/o
        CountdownEvent jcnt = new CountdownEvent(1);
        for (int j = 0; j < 50; j++)
        {
            jcnt.AddCount();
            ThreadPool.QueueUserWorkItem((jnum) =>
            {
                Console.WriteLine("j" + jnum + " scheduled...");
                Thread.Sleep(20000);  // simulated i/o
                jcnt.Signal();
                Console.WriteLine("j" + jnum + " complete.");
            }, j);
        }
        jcnt.Signal();
        jcnt.Wait();
        icnt.Signal();
        Console.WriteLine("i" + inum + " complete.");
    }, i);
}
icnt.Signal();
icnt.Wait();

现在,您永远不会使用此模式(它会在启动时死锁),但它确实演示了您可以使用线程池导致的特定死锁 - 通过在阻塞线程消耗完整个线程后等待嵌套线程完成时阻塞游泳池。

我想知道使用嵌套的 Parallel.For 版本是否存在产生类似有害行为的潜在风险:

Parallel.For(1, 50, (i) =>
{
    Console.WriteLine("i" + i + " scheduled...");
    Thread.Sleep(10000);  // simulated i/o
    Parallel.For(1, 5, (j) =>
    {
        Thread.Sleep(20000);  // simulated i/o
        Console.WriteLine("j" + j + " complete.");
    });
    Console.WriteLine("i" + i + " complete.");
});

显然调度机制要复杂得多(而且我还没有看到这个版本出现死锁),但潜在的风险似乎仍然潜伏在那里。理论上是否有可能通过依赖嵌套线程来使 Parallel.For 使用的池干涸到造成死锁的程度?即 Parallel.For 为延迟后安排的作业保留的线程数是否有限制?

【问题讨论】:

    标签: c# multithreading threadpool task-parallel-library parallel-extensions


    【解决方案1】:

    不,不存在像Parallel.For()(或Parallel.ForEach())那样的死锁风险。

    有一些因素会降低死锁的风险(如使用的线程的动态计数)。但是死锁不可能的还有一个原因:迭代也是在原始线程上运行的。这意味着如果ThreadPool 完全忙,计算将完全同步运行。在这种情况下,您不会因使用 Parallel.For() 而获得任何加速,但您的代码仍会运行,不会出现死锁。

    此外,Tasks 的类似情况也可以正确解决:如果您在尚未计划的 Task(或访问其 Result)上的 Wait(),它将在当前线程。我认为这主要是一种性能优化,但我认为它在某些特定情况下也可以避免死锁。

    但我认为这个问题更多的是理论而不是实际。 .Net 4 ThreadPool 的默认最大线程数设置为大约一千。如果您同时有数千个Threads 阻塞,那么您做错了什么。

    【讨论】:

    • 是的,在以前的实现中,只有当您显式减小池的大小(以减少并发 i/o 操作或其他)时,它才可能出现。在后者中,我想您可以使用 ParallelOptions 为每个循环分别调整并发性,因此无论哪种方式都具有争议性。有趣的是,它在主线程中运行。
    • WRT 等待尚未安排的任务,我猜这是假设调度程序可以在该线程上运行。如果后台线程对需要在 UI 线程上运行的任务执行 Wait(),我希望它不会在该后台线程上运行它:)
    • @JamesManning 是的,你是对的。这是通过调用调度程序的TryExecuteTaskInline() 来完成的。并且调度器可以通过返回false来拒绝内联运行任务。
    猜你喜欢
    • 1970-01-01
    • 2010-11-30
    • 2023-01-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-15
    • 1970-01-01
    相关资源
    最近更新 更多