嵌套并行中的死锁风险

时间:2012-06-30 00:16:29

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

使用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在延迟后安排的作业后面的口袋中是否有一个限制?

1 个答案:

答案 0 :(得分:4)

不,Parallel.For()(或Parallel.ForEach())不存在类似死锁的风险。

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

此外,与Task s类似的情况也可以正确解决:如果您Wait()(或访问其Task)尚未安排Result然而,它将在当前线程中内联运行。我认为这主要是性能优化,但我认为它也可以避免某些特定情况下的死锁。

但我认为这个问题更具理论性而非实际性。 .Net 4 ThreadPool的默认最大线程数设置为千分之一。如果你在同一时刻有Thread次阻塞,那你就做错了。