2010-10-14 16 views
7

Mam pewien kod, który po wywołaniu wywołania usługi internetowej, kwerendy bazy danych i pobiera wartość z lokalnej pamięci podręcznej. Następnie łączy wartości zwracane z tych trzech działań, aby uzyskać wynik. Zamiast kolejno wykonywać te czynności, chcę je wykonywać asynchronicznie równolegle. Oto niektóre atrapa/przykładowy kod:C# wątek asynchroniczny 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)); 

Problemem jest to, że ostatnia linia generuje błąd, ponieważ dbResult jest jeszcze zerowa, kiedy zostanie wykonany. Jak tylko queryDB.EndInvoke zostanie wywołany, WaitHandle zostanie zasygnalizowane i wykonanie będzie kontynuowane, ZANIM wynik queryDB.EndInvoke zostanie przypisany do dbResult. Czy jest to elegancki/elegancki sposób wokół tego?

Uwaga: Należy dodać, że wpływa to na dbResult tylko dlatego, że queryDB jest ostatnim znakiem oczekiwania, który ma być sygnalizowany.

Aktualizacja: Podczas przyjąłem odpowiedź Filipa, który jest wielki, w następstwie uwag Andrey, powinniśmy dodać, że to działa również:

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

Brak odpowiedzi, ale aktualizacja do Fx4 znacznie ułatwiłaby to. –

Odpowiedz

3

Niestety, funkcja WaitHandle zawsze zostanie zasygnalizowana przed zwrotem połączenia EndInvoke(). Co znaczy, że nie możesz na tym polegać.

Jeśli nie możesz użyć 4.0, system wątków lub ręcznych waithandles prawdopodobnie będzie w porządku (lub przerażający Sleep()!). Możesz także wybrać metodę Invoked, która ustawia wyniki (EndInvoke ma wartość po ustawieniu wartości wyniku), ale oznacza to przeniesienie wyników do współużytkowanej lokalizacji, a nie do zmiennych lokalnych - prawdopodobnie wymagającej niewielkiego przeprojektowania.

Lub Jeśli możesz użyć wersji 4.0, chciałbym - System.Threading.Tasks jest podcinać pełne "o świetne rzeczy. można przepisać do:

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

Dzięki. Wygląda na to, że może dobrze działać. Pozdrawiam :) – BertC

+0

"zawsze" jest nieprawidłowe. nie jest deterministyczny – Andrey

+0

@Andrey zawsze * jest * poprawny; zostanie to zasygnalizowane przed zwrotem połączenia - musi tak być, ponieważ sama metoda sygnalizuje uchwyt oczekiwania, więc nie może wrócić przed sygnalizacją. Nie znaczy to jednak, że oczekująca nić natychmiast otrzyma kontrolę. –

1

pójdę z 3 wątków tutaj i uniknąć Invoke(). Dla mnie wątki są bardziej czytelne, a nawet możesz umieścić swój kod w anonimowej metodzie wewnątrz Thread.Start().

Po uruchomieniu, powinieneś .Join() wszystkie 3 wątki tutaj, a będziesz mieć pewność, że wyniki są gotowe.

Byłoby coś jak:

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

Być może źle zrozumiałem, ale czy nie tak robi BeginInvoke? Wprowadza nowy wątek. Jeśli utworzę własne wątki, nadal będę musiał znaleźć mechanizm oczekiwania na zakończenie każdej z nich, podobnie jak w przypadku waithandles? – BertC

+1

To jest zły pomysł. Należy użyć ThreadPool lub BeginInvoke – Andrey

+0

Dlaczego? Czy nie jest to przesada ThreadPool? –

0

byłbym skłonny do umieścić zapytania do trzech metod, które mogą zostać wykorzystane w sposób asynchroniczny i ogień „kompletną” zdarzenie po zakończeniu. Następnie, gdy każde zdarzenie powróci, zaktualizuj status, a gdy wszystkie trzy są "prawdziwe", wykonaj wynik.

To może nie być miłe/eleganckie, ale jest proste i z asynchronicznymi połączeniami, które chcesz.

+0

Dzięki Chris. Zastanawiałem się nad tym, ale wydaje mi się, że jestem tak niezgrabny i muszę napisać semafor sygnału WaitHandle, który wydaje się przełamać cel ich posiadania w pierwszej kolejności. – BertC

1

Najpierw wyjaśnię, dlaczego tak się dzieje, a następnie powiedzieć, jak to naprawić.

Napiszmy prosty program:

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

wyjście jest:

1 at 3 
3 at 1 

Res1 0 
2 at 3 
Res2 5 

który nie jest to, że chciałem.Dzieje się tak, ponieważ wewnętrznie Begin/End Wywołanie działa w ten sposób:

  1. Execute delegata
  2. Signal WaitHandle
  3. Execute wywołanie zwrotne

Ponieważ dzieje się to na wątku innego następnie główny jest możliwe (i bardzo prawdopodobnie), że przełącznik wątku ma miejsce między 2 a 3.

Aby to naprawić, powinieneś wykonać:

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

a wynik będzie zarówno poprawny, jak i deterministyczny.