2017-02-25 53 views
1

前言:我知道使用ThreadPool(通過TPL或直接)用於IO操作is generally frowned upon,因爲IO必須是順序的,但是我的問題涉及阻塞調用的「並行IO」不要公開Async方法。使用TPL與並行阻塞IO操作

我正在寫一個GUI工具,獲取有關網絡上的計算機,做這個(簡化代碼)信息:

String[] computerNames = { "foo", "bar", "baz" }; 
foreach(String computerName in computerNames) { 

    Task.Factory 
     .StartNew(GetComputerInfo, computerName) 
     .ContinueWith(ShowOutputInGui, RunOnGuiThread); 

} 

private ComputerInfo GetComputerInfo(String machineName) { 

    Task<Int64>  pingTime = Task.Factory.StartNew(() => GetPingTime(machineName)); 
    Task<Process[]> processes = Task.Factory.StartNew(() => System.Diagnostics.Process.GetProcesses(machineName)); 
    // and loads more 

    Task.WaitAll(pingtime, processes, etc); 

    return new ComputerInfo(pingTime.Result, processes.Result, etc); 
} 

當我運行這段代碼我發現它需要一個令人驚訝的長量與我曾經使用過的舊順序代碼相比,它的運行時間更長

注意的是,在GetComputerInfo方法每個任務完全獨立於其他周圍的(如平時間可分別從GetProcesses計算),但是當我插入一些Stopwatch定時調用,我發現,各個子任務,如在調用GetComputerInfo之後,GetProcesses調用僅啓動至3000ms - 存在一些大的延遲。

我注意到,當我減少外部並行調用的數量到GetComputerInfo(通過減小computerNames數組的大小),第一個結果幾乎立即返回。一些計算機名稱是關閉的計算機,所以稱爲GetProcessesPingTime需要很長時間才能超時(我的真實代碼捕捉到了例外)。這可能是因爲離線計算機阻塞Tasks正在運行,並且TPL自然將其限制爲我的CPU硬件線程數(8)。

有沒有辦法告訴TPL不要讓內部任務(例如GetProcesses)阻止外部任務(GetComputerInfo)?我試過「Parent/Child」任務附件/阻塞,但它不適用於我的情況,因爲我從不明確地將子任務附加到父任務,父任務自然會等待Task.WaitAll) 。

+0

也許會更好,如果'GetComputerInfo( )'沒有'Task.WaitAll()'在其中....有點失敗的目的。爲什麼不返回'任務[]'? – MickyD

+1

沒有一個好的[mcve]可靠地重現問題,如果不是不可能診斷問題是困難的。也就是說,請記住,線程池沒有無限數量的線程在等着你。在空閒狀態下,它將只有少數線程(最多等於CPU內核數量),並且每半秒鐘只啓動一次新線程(這是可配置的IIRC,我不記得默認情況)。您可以通過以下方式來幫助:a)使用異步ping方法,並且b)使用ThreadPool.SetMinThreads()來增加空閒時的線程數量,以便在需要時準備好它們。 –

+0

請注意,調用'SetMinThreads()'有點破解。很不幸,.NET沒有'GetProcesses()'方法的異步版本;但我甚至沒有看到用本機代碼實現的異步方式。如果你真的希望這些操作能夠平行進行,我認爲你自己就不能自己管理這些線程。 –

回答

2

我假設你在某個事件處理程序中有你的foreach循環,所以你應該做的第一件事就是將它標記爲async,這樣你就可以以異步的方式調用你的另一個。之後,你應該介紹你的GetComputerInfoasyncall the way down

代碼中存在其他缺陷:StartNew is dangerous,因爲它使用Current調度程序執行任務,而不是Default(因此您需要其他過載)。不幸的是,那個過載需要更多的參數,所以代碼不會那麼簡單。好消息是,你仍然需要一個超負荷告訴你的任務是長期運行線程池,因此應該使用這些專用線程:

TaskCreationOptions.LongRunning

指定一個任務將是一個長期運行的粗粒度操作,涉及比細粒度系統更少,更大的組件。它向TaskScheduler提供了暗示,認爲超額認購可能是有保證的。

超額配額允許您創建比可用數量的硬件線程更多的線程。它還向任務調度程序提供任務可能需要額外的線程,以便它不會阻止本地線程池隊列上的其他線程或工作項目的前進進度。

而且你應該避免WaitAll方法,因爲它是一個阻塞操作,所以你必須1線程少做實際的工作。您可能想要使用WhenAll

最後,返回你的ComputerInfo結果,你可以使用延續與TaskCompletionSource使用,所以你的代碼可能是這樣的(取消邏輯還補充):

using System.Diagnostics; 

// handle event in fire-and-forget manner 
async void btn_Click(object sender, EventArgs e) 
{ 
    var computerNames = { "foo", "bar", "baz" }; 
    foreach(String computerName in computerNames) 
    { 
     var compCancelSource = new CancellationTokenSource(); 

     // asynchronically wait for next computer info 
     var compInfo = await GetComputerInfo(computerName, compCancelSource. Token); 
     // We are in UI context here 
     ShowOutputInGui(compInfo); 
     RunOnGuiThread(compInfo); 
    } 
} 

private Task<ComputerInfo> GetComputerInfo(String machineName, CancellationToken token) 
{ 
    var pingTime = Task.Factory.StartNew(
     // action to run 
     () => GetPingTime(machineName), 
     //token to cancel 
     token, 
     // notify the thread pool that this task could take a long time to run, 
     // so the new thread probably will be used for it 
     TaskCreationOptions.LongRunning, 
     // execute all the job in a thread pool 
     TaskScheduler.Default); 

    var processes = Task.Run(() => Process.GetProcesses(machineName), token, TaskCreationOptions.LongRunning, TaskScheduler.Default); 
    // and loads more 

    await Task.WhenAll(pingtime, processes, etc); 
    return new ComputerInfo(pingTime.Result, processes.Result, etc); 

    //var tcs = new TaskCompletionSource<ComputerInfo>(); 
    //Task.WhenAll(pingtime, processes, etc) 
    // .ContinueWith(aggregateTask => 
    //  if (aggregateTask.IsCompleted) 
    //  { 
    //   tcs.SetResult(new ComputerInfo(
    //    aggregateTask.Result[0], 
    //    aggregateTask.Result[1], 
    //    etc)); 
    //  } 
    //  else 
    //  { 
    //   // cancel or error handling 
    //  }); 

    // return the awaitable task 
    //return tcs.Task; 
} 
+0

爲什麼'GetComputerInfo'使用延續和tcs而'btn_Click'是異步? 'GetComputerInfo'應該返回'Task ',順便說一句。 – Haukinger

+0

這是一個使用TaskCompletionSource的異步包裝器,請參閱圖9中鏈接的MSDN文章。簽名固定,謝謝。 – VMAtm

+1

'Task.Run'是異步包裝器,但如果代碼的其餘部分使用'async' /'await','ContinueWith'是一種可憎的東西。 '等待Task.WhenAll(...);返回新的ComputerInfo(...);'會更容易編寫,並且更易於閱讀和理解。 – Haukinger