2010-10-14 16 views
7

Tengo un código que cuando se llama llama a un servicio web, consulta una base de datos y obtiene un valor de la memoria caché local. Luego combina los valores de retorno de estas tres acciones para producir su resultado. En lugar de realizar estas acciones secuencialmente, quiero realizarlas de forma asincrónica en paralelo. Aquí hay algo de código ficticio/ejemplo: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)); 

El problema es que la última línea genera un error porque dbResult sigue siendo nula cuando es ejecutado. Tan pronto como se invoca queryDB.EndInvoke, se señala el WaitHandle y la ejecución continúa ANTES de que el resultado de queryDB.EndInvoke se asigne a dbResult. ¿Hay una manera ordenada/elegante alrededor de esto?

Nota: Debo agregar que esto afecta a dbResult simplemente porque la queryDB es el último identificador de espera para ser señalado.

Actualización: Aunque acepté la respuesta de Felipe que es grande, a raíz de los comentarios de Andrei, debo añadir que esto también funciona:

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

No es una respuesta, pero una actualización a Fx4 lo haría mucho más fácil. –

Respuesta

3

Lamentablemente, WaitHandle siempre se señalizará antes de que la llamada EndInvoke() regrese. Lo que significa que no puedes confiar en esto.

Si no puede usar 4.0, un sistema de subprocesos o manivelas manuales probablemente estará en orden (o el temido truco de suspensión()). También puede hacer que el método invocado sea el que establezca los resultados (para que EndInvoke pase después de se establezca el valor del resultado), pero eso significa mover los resultados a una ubicación compartida y no variables locales, lo que probablemente requiera un pequeño rediseño.

O Si puede usar 4.0, yo - System.Threading.Tasks está lleno de cosas grandiosas. Puede volver a escribir 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

Gracias. Parece que podría funcionar bien. Saludos :) – BertC

+0

"siempre" es incorrecto. no es determinista – Andrey

+0

@Andrey siempre * es * correcto; se señalará antes de que regrese la llamada; tiene que ser así, ya que el método en sí es lo que señala el identificador de espera para que no pueda regresar antes de la señalización. Sin embargo, eso no significa que el hilo de espera reciba el control inmediatamente. –

1

Me gustaría ir con 3 hilos de aquí y evitar Invoke(). Para mí, los hilos son más legibles, e incluso puedes poner su código en un método anónimo dentro del Thread.Start().

Después de comenzar, debe .Join() los 3 hilos aquí, y se asegurará de que sus resultados estén listos.

Sería algo así como:

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

Quizás he entendido mal pero ¿no es eso lo que hace BeginInvoke? Lanza un nuevo hilo. Si creo mis propios hilos, igual tendré que utilizar un mecanismo para esperar que cada uno se complete de forma similar a los tiradores de espera, ¿verdad? – BertC

+1

que es una mala idea. ThreadPool o BeginInvoke deben usarse – Andrey

+0

¿Por qué? ¿No es excesivo ThreadPool para eso? –

0

estaría tentado a poner las consultas en tres métodos que se pueden llamar de forma asíncrona y disparar un evento "completa" cuando haya terminado. Luego, cuando cada evento regrese, actualice un estado y cuando los tres sean "verdaderos" realice su salida.

Puede que no sea prolijo/elegante, pero es sencillo y con llamadas asíncronas es lo que desea.

+0

Gracias Chris. Lo había considerado, pero se siente tan torpe y como si tuviera que escribir el semáforo de la señal WaitHandle, lo que parece frustrar el propósito de tenerlos en primer lugar. – BertC

1

Primero le explicaré por qué ocurre y luego le diré cómo solucionarlo.

escribamos programa simple:

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

salida es:

1 at 3 
3 at 1 

Res1 0 
2 at 3 
Res2 5 

que no se que quería.Esto sucede porque Comience internamente/Fin de invocación funciona de esta manera:

  1. Ejecutar delegado
  2. señal WaitHandle
  3. Ejecutar devolución de llamada

Dado que esto sucede en la rosca del otro entonces principal es posible (y muy probable) que el cambio de hilo ocurra justo entre 2 y 3.

Para solucionarlo, debe hacer:

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

y el resultado será correcto y determinista.