Aquí está la secuencia de fragmentos de código que utilicé recientemente para ilustrar la diferencia y varios problemas usando soluciones asíncronas.
Suponga que tiene algún controlador de eventos en su aplicación basada en GUI que requiere mucho tiempo y, por lo tanto, le gustaría hacerlo asincrónico. Aquí está la lógica síncrona con la que comienza:
while (true) {
string result = LoadNextItem().Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
LoadNextItem devuelve una tarea, que eventualmente producirá algún resultado que le gustaría inspeccionar. Si el resultado actual es el que está buscando, actualice el valor de algún contador en la interfaz de usuario y regrese del método. De lo contrario, continuará procesando más elementos de LoadNextItem.
Primera idea para la versión asincrónica: ¡solo usa continuaciones! E ignoremos la parte del bucle por el momento. Quiero decir, ¿qué podría salir mal?
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
});
¡Genial, ahora tenemos un método que no bloquea! En cambio, se estrella. Cualquier actualización de los controles de la interfaz de usuario debería ocurrir en el hilo de la interfaz de usuario, por lo que deberá tenerlo en cuenta. Afortunadamente, hay una opción para especificar cómo se deben programar las continuaciones, y hay una predeterminada solo para esto:
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
},
TaskScheduler.FromCurrentSynchronizationContext());
¡Genial, ahora tenemos un método que no falla! En cambio, falla silenciosamente. Las continuaciones son tareas separadas en sí mismas, y su estado no está ligado al de la tarea anterior. Entonces, incluso si LoadNextItem falla, la persona que llama solo verá una tarea que se haya completado con éxito. Bien, entonces simplemente pase la excepción, si hay una:
return LoadNextItem().ContinueWith(t => {
if (t.Exception != null) {
throw t.Exception.InnerException;
}
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
},
TaskScheduler.FromCurrentSynchronizationContext());
Genial, ahora esto realmente funciona. Por un solo artículo. Ahora, ¿qué tal ese bucle? Resulta que una solución equivalente a la lógica de la versión síncrona original se verá así:
Task AsyncLoop() {
return AsyncLoopTask().ContinueWith(t =>
Counter.Value = t.Result,
TaskScheduler.FromCurrentSynchronizationContext());
}
Task<int> AsyncLoopTask() {
var tcs = new TaskCompletionSource<int>();
DoIteration(tcs);
return tcs.Task;
}
void DoIteration(TaskCompletionSource<int> tcs) {
LoadNextItem().ContinueWith(t => {
if (t.Exception != null) {
tcs.TrySetException(t.Exception.InnerException);
} else if (t.Result.Contains("target")) {
tcs.TrySetResult(t.Result.Length);
} else {
DoIteration(tcs);
}});
}
O, en lugar de todo lo anterior, puede usar async para hacer lo mismo:
async Task AsyncLoop() {
while (true) {
string result = await LoadNextItem();
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
}
Eso es mucho mejor ahora, ¿no?
Wait
llamada en el segundo ejemplo , los dos fragmentos serían (en su mayoría) equivalentes.