4

看看下面的天真實現一個嵌套的異步循環使用線程池:死鎖風險

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保留在延遲後計劃的作業的後面口袋中的線程數是否有限制?

回答

4

不,不存在像Parallel.For()(或Parallel.ForEach())那樣的死鎖風險。

有一些因素會降低死鎖的風險(如使用的線程的動態計數)。但是,死鎖是不可能的,這也是一個原因:迭代也在原始線程上運行。這意味着如果ThreadPool完全忙,則計算將完全同步運行。在這種情況下,使用Parallel.For()時你不會得到任何加速,但是你的代碼仍然可以運行,沒有死鎖。

而且,與Task提供了一個類似的情況也正確解決:如果你Wait()Task(或訪問其Result),其尚未排定,它會在當前線程串聯模式運行。我認爲這主要是性能優化,但我認爲它也可以避免在某些特定情況下的死鎖。

但我認爲這個問題比實際更理論化。 .Net 4 ThreadPool的默認最大線程數設置爲大約一千。如果你在同一時間阻止了一千個Thread,那麼你正在做一件非常錯誤的事情。

+0

是的,在前面的實現中,只有當你明確地減小了池的大小(以減少併發的I/O操作或者其他)時纔有可能。在後者中,我想你可以使用ParallelOptions分別調整每個循環的併發性,所以這兩種方法都是毫無意義的。有趣的是,它知道它在主線程中運行。 – silijon

+0

WRT正在等待尚未安排的任務,我猜測這是假設調度程序在該線程上運行良好。如果後臺線程在需要在UI線程上運行的任務上執行Wait(),我希望它不會在該後臺線程上運行它:) –

+0

@JamesManning是的,你說得對。這通過調用調度器的['TryExecuteTaskInline()'](http://msdn.microsoft.com/en-us/library/dd449178.aspx)來完成。調度程序可以通過返回'false'來拒絕內聯運行任務。 – svick