2010-10-14 124 views
7

我有一些代碼,當被調用時調用Web服務,查詢數據庫並從本地緩存中獲取一個值。然後它將這三個行爲的返回值結合起來以產生結果。我不想按順序執行這些操作,而是希望以並行方式異步執行它們。下面是一些僞/示例代碼:c#線程異步問題

var waitHandles = new List<WaitHandle>(); 

var wsResult = 0; 
Func<int> callWebService = CallWebService; 
var wsAsyncResult = callWebService.BeginInvoke(res => { wsResult = callWebService.EndInvoke(res); }, null); 
waitHandles.Add(wsAsyncResult.AsyncWaitHandle); 

string dbResult = null; 
Func<string> queryDB = QueryDB; 
var dbAsyncResult = queryDB.BeginInvoke(res => { dbResult = queryDB.EndInvoke(res); }, null); 
waitHandles.Add(dbAsyncResult.AsyncWaitHandle); 

var cacheResult = ""; 
Func<string> queryLocalCache = QueryLocalCache; 
var cacheAsyncResult = queryLocalCache.BeginInvoke(res => { cacheResult = queryLocalCache.EndInvoke(res); }, null); 
waitHandles.Add(cacheAsyncResult.AsyncWaitHandle); 

WaitHandle.WaitAll(waitHandles.ToArray());   
Console.WriteLine(string.Format(dbResult, wsResult, cacheResult)); 

的問題是,因爲當它被執行dbResult仍然是空的最後一行拋出一個錯誤。只要queryDB.EndInvoke被調用,WaitHandle被髮信號並且執行繼續之前,queryDB.EndInvoke的結果被分配給dbResult。這是否有一個整潔/優雅的方式?

注意:我應該補充一點,這會影響dbResult,因爲queryDB是最後一個等待處理信號。

更新:雖然我接受了菲利普的回答這是偉大的,下面安德烈的意見,我要補充一點,這也適用:

var waitHandles = new List<WaitHandle>(); 

var wsResult = 0; 
Func<int> callWebService = CallWebService; 
var wsAsyncResult = callWebService.BeginInvoke(null, null); 
waitHandles.Add(wsAsyncResult.AsyncWaitHandle); 

string dbResult = null; 
Func<string> queryDB = QueryDB; 
var dbAsyncResult = queryDB.BeginInvoke(null, null); 
waitHandles.Add(dbAsyncResult.AsyncWaitHandle); 

var cacheResult = ""; 
Func<string> queryLocalCache = QueryLocalCache; 
var cacheAsyncResult = queryLocalCache.BeginInvoke(null, null); 
waitHandles.Add(cacheAsyncResult.AsyncWaitHandle); 

WaitHandle.WaitAll(waitHandles.ToArray()); 

var wsResult = callWebService.EndInvoke(wsAsyncResult); 
var dbResult = queryDB.EndInvoke(dbAsyncResult); 
var cacheResult = queryLocalCache.EndInvoke(cacheAsyncResult); 

Console.WriteLine(string.Format(dbResult, wsResult, cacheResult)); 
+0

沒有答案,但升級到Fx4會使這容易得多。 – 2010-10-14 20:31:48

回答

3

不幸的是,WaitHandle將始終在EndInvoke()調用返回之前發出信號。這意味着你不能依靠這個。

如果你不能使用4.0,一個線程或手動waithandles系統可能會按順序(或可怕的Sleep()破解!)。您也可以調用的方法是什麼套你的結果(所以結果值設置後EndInvoke會發生),但是這意味着結果移動到共享位置,而不是局部變量 - 可能需要重新設計小。

如果你可以使用4.0,我會 - System.Threading.Tasks是充滿'偉大的東西。你可以改寫爲:

var tasks = new List<Task>(); 

var wsResult = 0; 
string dbResult = null; 
var cacheResult = ""; 

tasks.Add(new Task(()=> wsResult = CallWebService())); 
tasks.Add(new Task(()=> dbResult = QueryDB())); 
tasks.Add(new Task(()=> cacheResult = QueryLocalCache())); 

tasks.ForEach(t=> t.Start()); 
Task.WaitAll(tasks.ToArray()); 

Console.WriteLine(string.Format(dbResult, wsResult, cacheResult)); 
+0

謝謝。看起來它可能很好地工作。乾杯:) – BertC 2010-10-14 20:48:47

+0

「總是」不正確。這是不確定的 – Andrey 2010-10-14 20:55:51

+0

@Andrey總是*是*正確;它會在呼叫返回之前發出信號 - 它必須是,因爲該方法本身就是等待手柄的信號,所以它在信號發送之前不能返回。但這並不意味着等待的線程將立即得到控制權。 – 2010-10-15 13:41:59

1

我會用3個線程去這裏,避免Invoke()。對我而言,線程更具可讀性,您甚至可以將其代碼放入Thread.Start()內部的匿名方法中。

開始後,你應該在這裏所有3個主題,你會確定你的結果已經準備就緒。

這將是這樣的:

Thread t1=new Thread(delegate() { wsResult = CallWebService(); }); 
Thread t2=new Thread(delegate() { dbResult = QueryDb(); }); 
Thread t3=new Thread(delegate() { cacheResult = QueryLocalCache(); }); 
t1.Start(); t2.Start(); t2.Start(); 
t1.Join(); t2.Join(); t3.Join(); 
+0

也許我誤解了,但不是那個BeginInvoke做了什麼?它啓動了一個新線程。如果我創建自己的線程,我仍然不得不使用找到一個機制來等待每個完成類似於等待句柄的權利? – BertC 2010-10-14 20:18:55

+1

這是個壞主意。應該使用ThreadPool或BeginInvoke – Andrey 2010-10-14 20:45:55

+0

爲什麼? ThreadPool不是過度殺傷它嗎? – 2010-10-14 20:51:58

0

我會很想把查詢到三種方法可以異步調用,完成後觸發一個「完整」的事件。然後當每個事件回來時更新一個狀態,當三個都是「真」時執行你的輸出。

它可能不是很整齊/優雅,但它很簡單,並且異步調用是您想要的。

+0

謝謝克里斯。我曾考慮過這個問題,但它感覺如此笨拙,就像我不得不自己編寫WaitHandle信號信號一樣,這似乎破壞了讓它們擺在首位的目的。 – BertC 2010-10-14 20:07:32

1

首先我會解釋爲什麼會發生,然後告訴如何解決它。

讓我們來編寫簡單的程序:

 var wsResult = 0; 
     Func<int> callWebService =() => { 
      Console.WriteLine("1 at " + Thread.CurrentThread.ManagedThreadId); 
      return 5; 
     }; 
     var wsAsyncResult = callWebService.BeginInvoke(res => { 
      Console.WriteLine("2 at " + Thread.CurrentThread.ManagedThreadId); 
      wsResult = callWebService.EndInvoke(res); 
     }, null); 
     wsAsyncResult.AsyncWaitHandle.WaitOne(); 
     Console.WriteLine("3 at " + Thread.CurrentThread.ManagedThreadId); 
     Console.WriteLine(); 
     Console.WriteLine("Res1 " + wsResult); 
     Thread.Sleep(1000); 
     Console.WriteLine("Res2 " + wsResult); 

輸出爲:

1 at 3 
3 at 1 

Res1 0 
2 at 3 
Res2 5 

這是不是想要的。這是因爲在內部開始/結束調用的工作原理是這樣的:

  1. 執行委託
  2. 信號的WaitHandle
  3. 執行回調

因爲這發生在另一個線程則主要是可能的(而且很可能)那個線程切換髮生在2和3之間。

要修復它,你應該這樣做:

 var wsResult = 0; 
     Func<int> callWebService =() => { 
      Console.WriteLine("1 at " + Thread.CurrentThread.ManagedThreadId); 
      return 5; 
     }; 
     var wsAsyncResult = callWebService.BeginInvoke(null, null); 
     wsAsyncResult.AsyncWaitHandle.WaitOne(); 
     wsResult = callWebService.EndInvoke(wsAsyncResult); 

結果將是正確的和確定性的。