Diferencia entre await y ContinueWith

119

¿Alguien puede explicar si awaity ContinueWithson sinónimos o no en el siguiente ejemplo? Estoy tratando de usar TPL por primera vez y he estado leyendo toda la documentación, pero no entiendo la diferencia.

Espera :

String webText = await getWebPage(uri);
await parseData(webText);

Continuar con :

Task<String> webText = new Task<String>(() => getWebPage(uri));
Task continue = webText.ContinueWith((task) =>  parseData(task.Result));
webText.Start();
continue.Wait();

¿Se prefiere uno sobre el otro en situaciones particulares?

Harrison
fuente
3
Si eliminó la Waitllamada en el segundo ejemplo , los dos fragmentos serían (en su mayoría) equivalentes.
Servicio
FYI: su getWebPagemétodo no se puede utilizar en ambos códigos. En el primer código tiene un Task<string>tipo de retorno mientras que en el segundo tiene un stringtipo de retorno. así que básicamente su código no se compila. - para ser precisos.
Royi Namir

Respuestas:

101

En el segundo código, está esperando sincrónicamente a que se complete la continuación. En la primera versión, el método volverá a la persona que llama tan pronto como llegue a la primera awaitexpresión que aún no está completa.

Son muy similares en el sentido de que ambos programan una continuación, pero tan pronto como el flujo de control se vuelve un poco complejo, awaitconduce a un código mucho más simple. Además, como señaló Servy en los comentarios, esperar una tarea "desenvolverá" las excepciones agregadas, lo que generalmente conduce a un manejo de errores más simple. Además, el uso awaitprogramará implícitamente la continuación en el contexto de llamada (a menos que use ConfigureAwait). No es nada que no se pueda hacer "manualmente", pero es mucho más fácil hacerlo await.

Le sugiero que intente implementar una secuencia de operaciones un poco más grande con ambos awaity Task.ContinueWith, puede ser una verdadera revelación.

Jon Skeet
fuente
2
El manejo de errores entre los dos fragmentos también es diferente; generalmente es más fácil trabajar con awaitover ContinueWithen ese sentido.
Servicio
@Servy: Es cierto, agregará algo al respecto.
Jon Skeet
1
La programación también es bastante diferente, es decir, en qué contexto se parseDataejecuta.
Stephen Cleary
Cuando dice que usar await programará implícitamente la continuación en el contexto de la llamada , ¿puede explicar el beneficio de eso y qué sucede en la otra situación?
Harrison
4
@Harrison: Imagine que está escribiendo una aplicación WinForms; si escribe un método asíncrono, de forma predeterminada, todo el código dentro del método se ejecutará en el hilo de la interfaz de usuario, porque la continuación se programará allí. Si no especifica dónde desea que se ejecute la continuación, no sé cuál es el valor predeterminado, pero podría terminar ejecutándose fácilmente en un subproceso del grupo de subprocesos ... momento en el que no puede acceder a la interfaz de usuario, etc. .
Jon Skeet
100

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?

pit
fuente
Gracias, muy buena explicación
Elger Mensonides
Este es un gran ejemplo
Royi Namir