2014-02-25 58 views
5

背景:繼續任務上的Task.WaitAll()只會延遲原始任務的執行嗎?

我有它創建Tasks處理來自DB數據控制檯應用程序(我們姑且稱之爲1級任務)。每個任務都會再次創建自己的任務來處理分配給它的每個數據部分(Level2任務)。

每個Level2任務都有一個與其關聯的繼續任務,並且在繼續前面的任務中用於對繼續任務執行WaitAll的代碼。

我在.NET 4.0(無異步/等待)

問題:

這創造了一個問題 - 事實證明,如果做到這樣,沒有任何的Level2的任務是之前啓動所有可用的Level1任務都已安排。這在任何方面都不是最佳的。

問:

這似乎是固定更改代碼等針對原始Level2的任務和它的後續任務。但是,我不完全確定爲什麼會出現這種情況?

你有什麼想法嗎?

我唯一能想出來的就是 - 由於延續任務尚未開始,因此等待它完成沒有意義?但即使如此,我預計至少有一些Level2任務已經開始。他們從未做過。

例子:

我創建了一個證明正是行爲的示例控制檯應用程序:

  1. 運行它當作是,你會發現它的第一個調度的所有任務,然後才你開始從Level2任務中獲取實際的書籤。

  2. 但註釋掉標記的代碼塊並取消註釋替換,並按預期工作。

你能告訴我爲什麼嗎?

public class Program 
{ 
    static void Main(string[] args) 
    { 
     for (var i = 0; i < 100; i++) 
     { 
      Task.Factory.StartNew(() => SomeMethod()); 
      //Thread.Sleep(1000); 
     } 

     Console.ReadLine(); 
    } 

    private static void SomeMethod() 
    { 
     var numbers = new List<int>(); 

     for (var i = 0; i < 10; i++) 
     { 
      numbers.Add(i); 
     } 

     var tasks = new List<Task>(); 

     foreach (var number in numbers) 
     { 
      Console.WriteLine("Before start task"); 

      var numberSafe = number; 

      /* Code to be replaced START */ 

      var nextTask = Task.Factory.StartNew(() => 
      { 
       Console.WriteLine("Got number: {0}", numberSafe); 
      }) 
       .ContinueWith(task => 
       { 
        Console.WriteLine("Continuation {0}", task.Id); 
       }); 

      tasks.Add(nextTask); 

      /* Code to be replaced END */ 

      /* Replacement START */ 

      //var originalTask = Task.Factory.StartNew(() => 
      //{ 
      // Console.WriteLine("Got number: {0}", numberSafe); 
      //}); 

      //var contTask = originalTask 
      // .ContinueWith(task => 
      // { 
      //  Console.WriteLine("Continuation {0}", task.Id); 
      // }); 

      //tasks.Add(originalTask); 
      //tasks.Add(contTask); 

      /* Replacement END */ 
     } 

     Task.WaitAll(tasks.ToArray()); 
    } 
} 
+0

你必須使用'ContinueWith',還是可以使用'async/await'? – Noseratio

+0

@Noseratio - 它是.NET 4.0 - 沒有異步/等待 –

+1

如果你使用VS2012 +,你仍然可以使用.NET 4.0:http://stackoverflow.com/tags/async-await/info – Noseratio

回答

4

我想你會看到Task Inlining的行爲。從MSDN報價:

在某些情況下,當一個任務在等待,它可以同步在正在執行等待操作的線程執行。這增強了性能,因爲它通過利用現有的將阻塞的線程來防止需要額外的線程,否則。爲了防止由於重入導致的錯誤,只有在相關線程的本地隊列中找到等待目標時纔會發生任務內聯。

你不需要100個任務來看到這個。我已經修改了你的程序來完成4個1級任務(我有四核CPU)。每個1級任務只創建一個2級任務。

static void Main(string[] args) 
{ 
    for (var i = 0; i < 4; i++) 
    { 
     int j = i; 
     Task.Factory.StartNew(() => SomeMethod(j)); // j as level number 
    } 
} 

在原來的程序nextTask是延續的任務 - 所以我只是簡單的方法。

private static void SomeMethod(int num) 
{ 
    var numbers = new List<int>(); 

    // create only one level 2 task for representation purpose 
    for (var i = 0; i < 1; i++) 
    { 
     numbers.Add(i); 
    } 

    var tasks = new List<Task>(); 

    foreach (var number in numbers) 
    { 
     Console.WriteLine("Before start task: {0} - thread {1}", num, 
           Thread.CurrentThread.ManagedThreadId); 

     var numberSafe = number; 

     var originalTask = Task.Factory.StartNew(() => 
     { 
      Console.WriteLine("Got number: {0} - thread {1}", num, 
            Thread.CurrentThread.ManagedThreadId); 
     }); 

     var contTask = originalTask 
      .ContinueWith(task => 
      { 
       Console.WriteLine("Continuation {0} - thread {1}", num, 
            Thread.CurrentThread.ManagedThreadId); 
      }); 

     tasks.Add(originalTask); // comment and un-comment this line to see change in behavior 

     tasks.Add(contTask); // same as adding nextTask in your original prog. 

    } 

    Task.WaitAll(tasks.ToArray()); 
} 

下面是示例輸出 - 在評論tasks.Add(originalTask); - 這是你的第一個塊。

Before start task: 0 - thread 4 
Before start task: 2 - thread 3 
Before start task: 3 - thread 6 
Before start task: 1 - thread 5 
Got number: 0 - thread 7 
Continuation 0 - thread 7 
Got number: 1 - thread 7 
Continuation 1 - thread 7 
Got number: 3 - thread 7 
Continuation 3 - thread 7 
Got number: 2 - thread 4 
Continuation 2 - thread 4 

一些樣本輸出 - 在保持tasks.Add(originalTask);這是你的第二塊

Before start task: 0 - thread 4 
Before start task: 1 - thread 6 
Before start task: 2 - thread 5 
Got number: 0 - thread 4 
Before start task: 3 - thread 3 
Got number: 3 - thread 3 
Got number: 1 - thread 6 
Got number: 2 - thread 5 
Continuation 0 - thread 7 
Continuation 1 - thread 7 
Continuation 3 - thread 7 
Continuation 2 - thread 4 

正如你可以在第二種情況中看到,當您等待originalTask在同一個線程中啓動它,在task inlining將使它運行在同一個線程上 - 這就是爲什麼你更早看到Got Number..消息的原因。

+0

這很有趣 - 我會詳細閱讀,並在考慮這方面做更多的測試。 –

+0

我發現所有的答案都非常有幫助 - 因爲你是第一個解釋它的人(並且有一個有用的例子),我會將你的標記標記爲已接受。謝謝 –

+0

@JoannaTurban:很高興幫助。如果你正在考慮替代方案,你應該看看'TPL.Dataflow'和'BufferBlock <>'那裏。它是異步/等待使用的異步/非阻塞生產者 - 消費者數據結構。 – YK1

0

我不得不說,這個代碼確實是不容樂觀,爲您創建100個任務,它並不意味着你將有100個線程,並創建兩個新的任務,每個任務中,你是在superscribing調度程序。如果這些任務與db讀取有關,爲什麼不把它們標記爲長處理並丟棄內部任務?

+0

這僅僅是一個例子,證明行爲。真正的應用程序正在做一個數據庫調用,但這不是問題的一部分。它們並不意味着以任何方式長時間運行。問題是 - 爲什麼代碼中的更改會以這種方式影響處理。 –

1

如果我沒有記錯的話,等待尚未安排的任務可以同步執行。 (請參見here)在替代情況下,這種行爲適用於您的代碼並不令人感到意外。

記住該線程行爲是高度implementation-和機器相關的,這裏所發生可能是東西的臺詞:

  • Task.StartNew通話和實際執行之間的延遲任務的線程池中,大多數所謂的「1級」任務(如果不是全部)在第一個實際執行之前安排。
  • 由於默認任務計劃程序使用.NET ThreadPool,因此此處計劃的所有任務可能都將在ThreadPool線程上執行。
  • 一旦執行「1級」任務,調度隊列全部填入「1級」任務。
  • 每次執行「1級」任務時,都會根據需要安排許多「2級」任務,但這些任務都是在「1級」任務之後安排的。
  • 當「等級1」任務到達等待「等級2」任務的所有延續點時,正在執行的線程將進入等待狀態。
  • 隨着等待狀態多線程池的線程,程序迅速達到線程池飢餓,強迫線程池來分配新主題(有可能超過100個),以解決飢餓
  • 一旦最後的「1級」的任務可以調用等待狀態,ThreadPool至少分配一個額外的線程。
  • 由於所有的「1級」任務都完成,最後分配的額外線程現在可以首次執行「2級」任務及其延續。
  • 經過一段時間後,一個「1級」任務將完成他所有的「2級」任務。然後,這個「1級」任務將從等待中喚醒並完成其執行,從而釋放另一個ThreadPool線程,並加速執行剩餘的「2級」任務和繼續。

當您使用替代方法時會發生什麼變化,是因爲您直接在要等待的任務數組中引用「第2級」任務,Task.WaitAll方法有機會執行「第2級「任務同步而不是空轉。 這在初始情況下不會發生,因爲連續任務不能同步運行。

總而言之,在ThreadPool線程中等待是導致線程匱乏和您觀察到的奇怪行爲的原因。儘管優化代碼等待任務使線程匱乏行爲消失,但顯然不是您應該依賴的。

爲了解決你最初的問題,你最好遵循lil-raz所提出的建議來消除你的內在任務。

如果您有權訪問C#5.0,則還可以考慮使用異步/等待模式編寫代碼而不必等待。

2

與您的代碼問題是阻止Task.WaitAll(tasks.ToArray())。默認TPL任務計劃程序將不會使用的每個任務的新池線程以Factory.StartNew開頭。然後你開始100級Level1任務,每個任務阻塞Task.WaitAll的線程。

這造成了一個瓶頸。使用默認大小ThreadPool,我得到約20個併發運行的線程,其中只有4個同時實際執行(CPU內核數量)。

因此,一些任務只會排隊,稍後會啓動,因爲前面的任務已經完成。要明白我的意思,嘗試改變你這樣的代碼:

static void Main(string[] args) 
{ 
    for (var i = 0; i < 100; i++) 
    { 
     Task.Factory.StartNew(() => SomeMethod(), 
      TaskCreationOptions.LongRunning); 
    } 

    Console.ReadLine(); 
} 

​​會給你所期望的行爲,但是這當然會是一個錯誤的解決

right解決方案是爲了儘可能避免阻塞代碼。如果你必須這樣做,你應該只在最上層進行阻塞等待。

爲了解決這個問題,你的代碼可以像下面那樣重新分解。請注意使用ContinueWhenAllUnwrap和(可選)ExecuteSynchronously,這有助於消除阻塞代碼並減少涉及的線程池數量。此版本表現更好。

using System; 
using System.Collections.Generic; 
using System.Threading.Tasks; 

public class Program 
{ 
    static void Main(string[] args) 
    { 
     var tasks = new List<Task>(); 

     for (var i = 0; i < 100; i++) 
     { 
      tasks.Add(Task.Factory.StartNew(() => SomeMethod(i)).Unwrap()); 
     } 

     // blocking at the topmost level 
     Task.WaitAll(tasks.ToArray()); 

     Console.WriteLine("Enter to exit..."); 
     Console.ReadLine(); 
    } 

    private static Task<Task[]> SomeMethod(int n) 
    { 
     Console.WriteLine("SomeMethod " + n); 

     var numbers = new List<int>(); 

     for (var i = 0; i < 10; i++) 
     { 
      numbers.Add(i); 
     } 

     var tasks = new List<Task>(); 

     foreach (var number in numbers) 
     { 
      Console.WriteLine("Before start task " + number); 

      var numberSafe = number; 

      var nextTask = Task.Factory.StartNew(() => 
      { 
       Console.WriteLine("Got number: {0}", numberSafe); 
      }) 
      .ContinueWith(task => 
      { 
       Console.WriteLine("Continuation {0}", task.Id); 
      }, TaskContinuationOptions.ExecuteSynchronously); 

      tasks.Add(nextTask); 
     } 

     return Task.Factory.ContinueWhenAll(tasks.ToArray(), 
      result => result, TaskContinuationOptions.ExecuteSynchronously); 
    } 
} 

理想,在現實生活中的項目,你應該與自然異步的API只要有可能(例如,"Using SqlDataReader’s new async methods in .Net 4.5"),並且只針對CPU綁定的計算任務使用Task.Run/Task.Factory.StartNew堅持。而對於服務器端應用程序(例如,ASP.NET Web API),Task.Run/Task.Factory.StartNew通常只會增加冗餘線程切換的開銷。它不會加快完成HTTP請求的速度,除非您真的需要並行執行多個CPU綁定作業,從而傷害可伸縮性。

我明白以下可能不是一個可行的選擇,但我強烈建議升級到VS2012 +並使用async/await來實現這樣的邏輯。這將是非常值得的投資,因爲它大大加快了編碼過程,併產生更簡單,更乾淨,更容易出錯的代碼。您仍然可以使用Microsoft.Bcl.Async來定位.NET 4.0。

+1

謝謝你的解釋 - 這是一個有趣的替代方案,可能是解決手頭問題的更可靠的解決方案。 –