2010-10-14 17 views
7

Ho un codice che quando chiamato chiama un webservice, interroga un database e recupera un valore dalla cache locale. Combina quindi i valori di ritorno di queste tre azioni per produrre il risultato. Piuttosto che eseguire queste azioni in sequenza, voglio eseguirle in modo asincrono in parallelo. Ecco alcuni codice fittizio/esempio:C# threading async problem

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)); 

Il problema è che l'ultima riga genera un errore perché dbResult è ancora nulla quando viene eseguito. Non appena queryDB.EndInvoke viene chiamata, WaitHandle viene segnalato e l'esecuzione continua PRIMA il risultato di queryDB.EndInvoke viene assegnato a dbResult. C'è un modo pulito/elegante intorno a questo?

Nota: Devo aggiungere che ciò influisce su dbResult semplicemente perché queryDB è l'ultimo handle di attesa da segnalare.

Update: Mentre ho accettato la risposta di Filippo che è grande, dopo i commenti di Andrey, devo aggiungere che questo funziona anche:

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

Non una risposta, ma un aggiornamento a Fx4 renderebbe tutto molto più semplice. –

risposta

3

Sfortunatamente, il WaitHandle sarà sempre segnalato prima della risposta alla chiamata EndInvoke(). Il che significa che non puoi fare affidamento su questo.

Se non è possibile utilizzare 4.0, un sistema di thread o waitancole manuali probabilmente sarà in ordine (o il temuto Sleep() hack!). Puoi anche impostare il risultato su Metodo invocato (quindi EndInvoke succede dopo il valore del risultato è impostato), ma ciò significa spostare i risultati in una posizione condivisa e non variabili locali, probabilmente richiedendo una piccola riprogettazione.

Oppure Se è possibile utilizzare 4.0, vorrei - System.Threading.Tasks è pieno zeppo 'o grandi cose. Potresti riscrivere a:

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

Grazie. Sembra che potrebbe funzionare bene. Saluti :) – BertC

+0

"sempre" non è corretto. non è deterministico – Andrey

+0

@Andrey sempre * è * corretto; sarà segnalato prima che la chiamata ritorni - deve essere, come il metodo stesso è ciò che segnala l'handle di attesa in modo che non possa tornare prima della segnalazione. Ciò non significa che il thread in attesa riceverà il controllo immediatamente, però. –

1

vorrei andare con 3 fili qui ed evitare Invoke(). Per me, i thread sono più leggibili e puoi persino inserire il suo codice in un metodo anonimo all'interno di Thread.Start().

Dopo l'avvio, è necessario inserire .Join() in tutti e 3 i thread e si sarà sicuri che i risultati siano pronti.

Sarebbe qualcosa di simile:

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

Forse ho frainteso ma non è quello che fa BeginInvoke? Si lancia una nuova discussione. Se creo i miei thread, dovrò ancora usare un meccanismo per attendere che ognuno completi come i wafer della mano destra? – BertC

+1

questa è una cattiva idea. ThreadPool o BeginInvoke dovrebbero essere utilizzati – Andrey

+0

Perché? ThreadPool non è eccessivo per questo? –

0

sarei tentato di mettere le query in tre metodi che possono essere chiamati in modo asincrono e il fuoco di un evento "complete" al termine. Quindi, quando ogni evento ritorna aggiornare uno stato e quando tutti e tre sono "veri" esegui il tuo output.

Potrebbe non essere pulito/elegante, ma è semplice e con le chiamate asincrone è ciò che desideri.

+0

Grazie Chris. L'avevo preso in considerazione, ma mi sembra così goffo e come se dovessi scrivere io stesso il semaforo del segnale WaitHandle che sembra sconfiggere lo scopo di averli in primo luogo. – BertC

1

Prima spiegherò perché succede, quindi dì come aggiustarlo.

Scriviamo semplice programma:

 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); 

output è:

1 at 3 
3 at 1 

Res1 0 
2 at 3 
Res2 5 

che non è quello voluto.Ciò accade perché internamente Begin/End Invoke funziona in questo modo:

  1. Eseguire delegato
  2. segnale WaitHandle
  3. Eseguire callback

Da questo avviene discussione altro allora principale è possibile (e molto probabilmente) che l'interruttore di thread avviene tra 2 e 3.

Per risolvere il problema è necessario:

 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); 

e il risultato sarà sia corretto che deterministico.