¿En qué se diferencia el uso de await del uso de ContinueWith al procesar tareas asincrónicas?

9

Esto es lo que quiero decir:

public Task<SomeObject> GetSomeObjectByTokenAsync(int id)
    {
        string token = repository.GetTokenById(id);
        if (string.IsNullOrEmpty(token))
        {
            return Task.FromResult(new SomeObject()
            {
                IsAuthorized = false
            });
        }
        else
        {
            return repository.GetSomeObjectByTokenAsync(token).ContinueWith(t =>
            {
                t.Result.IsAuthorized = true;
                return t.Result;
            });
        }
    }

Por encima de método puede ser esperado y creo que se asemeja mucho a lo que el T pide a base de una sincrónica P attern sugiere hacer? (Los otros patrones que conozco son los patrones APM y EAP ).

Ahora, ¿qué pasa con el siguiente código:

public async Task<SomeObject> GetSomeObjectByToken(int id)
    {
        string token = repository.GetTokenById(id);
        if (string.IsNullOrEmpty(token))
        {
            return new SomeObject()
            {
                IsAuthorized = false
            };
        }
        else
        {
            SomeObject result = await repository.GetSomeObjectByTokenAsync(token);
            result.IsAuthorized = true;
            return result;
        }
    }

Las diferencias clave aquí son que el método es asyncy utiliza las awaitpalabras clave, entonces, ¿qué cambia esto en contraste con el método escrito anteriormente? Sé que también puede ser esperado. Cualquier método que devuelva Tarea puede, en realidad, a menos que me equivoque.

Soy consciente de la máquina de estado creada con esas instrucciones de cambio cada vez que un método se etiqueta como async, y sé que en awaitsí mismo no usa ningún hilo: no se bloquea en absoluto, el hilo simplemente va a hacer otras cosas, hasta que sea volvió a llamar para continuar la ejecución del código anterior.

Pero, ¿cuál es la diferencia subyacente entre los dos métodos, cuando los invocamos usando la awaitpalabra clave? ¿Hay alguna diferencia, y si la hay, cuál es la preferida?

EDIT: Me siento como el primer fragmento de código se prefiere, ya que efectivamente eludir la asíncrono / Await palabras clave, sin ninguna repercusión - volvemos una tarea que continuará su ejecución de forma sincrónica, o una tarea ya realizada en el camino caliente (que puede ser en caché).

SpiritBob
fuente
3
Muy poco, en este caso. En su primer ejemplo, result.IsAuthorized = truese ejecutará en el grupo de subprocesos, mientras que en el segundo ejemplo podría ejecutarse en el mismo subproceso que invocó GetSomeObjectByToken(si tenía SynchronizationContextinstalado, por ejemplo, era un subproceso de interfaz de usuario). El comportamiento si GetSomeObjectByTokenAsyncarroja una excepción también será ligeramente diferente. En general, awaitse prefiere ContinueWith, ya que casi siempre es más legible.
canton7
1
En este artículo se llama "elidir", lo que parece una buena palabra para ello. Stephen definitivamente conoce su negocio. La conclusión del artículo es esencialmente que no importa mucho, en cuanto al rendimiento, pero hay ciertas dificultades de codificación si no espera. Su segundo ejemplo es un patrón más seguro.
John Wu
1
Quizás le interese esta discusión en Twitter del Partner Software Architect de Microsoft en el equipo ASP.NET: twitter.com/davidfowl/status/1044847039929028608?lang=en
Remy el
Se llama una "máquina de estado", no una "máquina virtual".
Enigmatividad
@ Enigmativity Gracias sabio, ¡olvidé editar eso!
SpiritBob

Respuestas:

5

El mecanismo async/ awaithace que el compilador transforme su código en una máquina de estado. Su código se ejecutará sincrónicamente hasta el primero awaitque llegue a un elemento que no se haya completado, si lo hubiera.

En el compilador de Microsoft C #, esta máquina de estado es un tipo de valor, lo que significa que tendrá un costo muy pequeño cuando todos awaitse completen, ya que no asignará un objeto y, por lo tanto, no generará basura. Cuando no se completa ninguno de estos, este tipo de valor inevitablemente se encuadra.

Tenga en cuenta que esto no evita la asignación de Tasks si ese es el tipo de objetos a esperar utilizados en las awaitexpresiones.

Con ContinueWith, solo evita las asignaciones (que no sean Task) si su continuación no tiene un cierre y si no utiliza un objeto de estado o reutiliza un objeto de estado tanto como sea posible (por ejemplo, de un grupo).

Además, se llama a la continuación cuando se completa la tarea, creando un marco de pila, no se alinea. El marco intenta evitar desbordamientos de pila, pero puede haber un caso en el que no evitará uno, como cuando se asignan grandes matrices.

La forma en que trata de evitar esto es verificando cuánta pila queda y, si por alguna medida interna la pila se considera llena, programa la continuación para ejecutarse en el programador de tareas. Intenta evitar excepciones fatales de desbordamiento de pila a costa del rendimiento.

Aquí hay una sutil diferencia entre async/ awaity ContinueWith:

  • async/ awaitprogramaré continuaciones en SynchronizationContext.Currentcaso de existir, de lo contrario en TaskScheduler.Current 1

  • ContinueWithprogramará continuaciones en el programador de tareas proporcionado o TaskScheduler.Currenten las sobrecargas sin el parámetro del programador de tareas

Para simular async/ await's comportamiento por defecto:

.ContinueWith(continuationAction,
    SynchronizationContext.Current != null ?
        TaskScheduler.FromCurrentSynchronizationContext() :
        TaskScheduler.Current)

Para simular async/ await'comportamiento s con Task' s .ConfigureAwait(false):

.ContinueWith(continuationAction,
    TaskScheduler.Default)

Las cosas comienzan a complicarse con los bucles y el manejo de excepciones. Además de mantener su código legible, async/ awaitfunciona con cualquier espera .

Su caso se maneja mejor con un enfoque mixto: un método sincrónico que llama a un método asincrónico cuando es necesario. Un ejemplo de su código con este enfoque:

public Task<SomeObject> GetSomeObjectByTokenAsync(int id)
{
    string token = repository.GetTokenById(id);
    if (string.IsNullOrEmpty(token))
    {
        return Task.FromResult(new SomeObject()
        {
            IsAuthorized = false
        });
    }
    else
    {
        return InternalGetSomeObjectByTokenAsync(repository, token);
    }
}

internal async Task<SomeObject> InternalGetSomeObjectByToken(Repository repository, string token)
{
    SomeObject result = await repository.GetSomeObjectByTokenAsync(token);
    result.IsAuthorized = true;
    return result;
}

En mi experiencia, he encontrado muy pocos lugares en el código de la aplicación donde agregar tal complejidad realmente paga el tiempo para desarrollar, revisar y probar dichos enfoques, mientras que en el código de la biblioteca cualquier método puede ser un cuello de botella.

El único caso en el que tiendo tareas Elide es cuando una Tasko Task<T>regresar método simplemente devuelve el resultado de otro método asíncrono, sin que ella misma de haber realizado cualquier I / O o cualquier post-procesamiento.

YMMV.


  1. A menos que use ConfigureAwait(false)o aguarde a alguien que use una programación personalizada
acelente
fuente
1
La máquina de estados no es un tipo de valor. Echar un vistazo a la fuente generado de un sencillo programa asíncrono: private sealed class <Main>d__0 : IAsyncStateMachine. Sin embargo, las instancias de esta clase pueden ser reutilizables.
Theodor Zoulias el
¿Diría que si manipulara solo el resultado de una tarea, estaría bien usarlo ContinueWith?
SpiritBob
3
@TheodorZoulias, la fuente de destino de lanzamiento generada en lugar de depuración genera estructuras.
Acelerado
2
@SpiritBob, si realmente quieres jugar con los resultados de las tareas, supongo que está bien, .ContinueWithestaba destinado a usarse mediante programación. Especialmente si maneja tareas intensivas de CPU en lugar de tareas de E / S. Pero cuando llegas al punto de tener que lidiar con Tasklas propiedades, la AggregateExceptioncomplejidad de las condiciones de manejo, los ciclos y el manejo de excepciones cuando se invocan métodos asincrónicos adicionales, casi no hay razón para seguir con eso.
Acelerado el
2
@SpiritBob, lo siento, quise decir "bucles". Sí, puede crear un campo de solo lectura estático con a Task.FromResult("..."). En el código asíncrono, si almacena en caché los valores por clave que requieren E / S para obtenerlos, puede usar un diccionario donde los valores son tareas, por ejemplo, en ConcurrentDictionary<string, Task<T>>lugar de ConcurrentDictionary<string, T>usar la GetOrAddllamada con una función de fábrica y esperar su resultado. Esto garantiza que solo se realiza una solicitud de E / S para llenar la clave de la memoria caché, despierta a los camareros tan pronto como se completa la tarea y sirve como una tarea completada después.
Acelerado el
2

Al usar ContinueWith, está usando las herramientas que estaban disponibles antes de la introducción de la funcionalidad async/ awaitcon C # 5 en 2012. Como herramienta es detallada, no es fácilmente composable, y requiere un trabajo adicional para desenvolver AggregateExceptionsy Task<Task<TResult>>devolver valores (los obtiene cuando pasa delegados asincrónicos como argumentos). Ofrece pocas ventajas a cambio. Puede considerar usarlo cuando quiera adjuntar múltiples continuaciones a la misma Task, o en algunos casos raros donde no puede usar async/ awaitpor alguna razón (como cuando está en un método con outparámetros ).


Actualización: eliminé los consejos engañosos que ContinueWithdeberían usar TaskScheduler.Defaultpara imitar el comportamiento predeterminado de await. En realidad la awaitde las programaciones predeterminadas usando su continuación TaskScheduler.Current.

Theodor Zoulias
fuente