Cualquier diferencia entre “await Task.Run (); regreso;" y "devolver Task.Run ()"?

90

¿Existe alguna diferencia conceptual entre las siguientes dos piezas de código?

async Task TestAsync() 
{
    await Task.Run(() => DoSomeWork());
}

y

Task TestAsync() 
{
    return Task.Run(() => DoSomeWork());
}

¿El código generado también es diferente?

EDITAR: Para evitar confusiones con Task.Run, un caso similar:

async Task TestAsync() 
{
    await Task.Delay(1000);
}

y

Task TestAsync() 
{
    return Task.Delay(1000);
}

ACTUALIZACIÓN TARDÍA: además de la respuesta aceptada, también hay una diferencia en cómo LocalCallContextse maneja: CallContext.LogicalGetData se restaura incluso cuando no hay asincronía. ¿Por qué?

avo
fuente
1
Sí, es diferente. Y difiere mucho. de lo contrario, no tendría sentido usar await/ asyncen absoluto :)
MarcinJuraszek
1
Creo que aquí hay dos preguntas. 1. ¿Le importa la implementación real del método a su interlocutor? 2. ¿Difieren las representaciones compiladas de los dos métodos?
DavidRR

Respuestas:

80

Una diferencia importante está en la propagación de excepciones. Una excepción, arrojado dentro de un async Taskmétodo, se almacena en el vuelto Taskobjeto y permanece latente hasta que la tarea se observa a través de await task, task.Wait(), task.Resulto task.GetAwaiter().GetResult(). Se propaga de esta manera incluso si se lanza desde la parte sincrónica del asyncmétodo.

Considere el siguiente código, donde OneTestAsyncy se AnotherTestAsynccomportan de manera bastante diferente:

static async Task OneTestAsync(int n)
{
    await Task.Delay(n);
}

static Task AnotherTestAsync(int n)
{
    return Task.Delay(n);
}

// call DoTestAsync with either OneTestAsync or AnotherTestAsync as whatTest
static void DoTestAsync(Func<int, Task> whatTest, int n)
{
    Task task = null;
    try
    {
        // start the task
        task = whatTest(n);

        // do some other stuff, 
        // while the task is pending
        Console.Write("Press enter to continue");
        Console.ReadLine();
        task.Wait();
    }
    catch (Exception ex)
    {
        Console.Write("Error: " + ex.Message);
    }
}

Si llamo DoTestAsync(OneTestAsync, -2), produce la siguiente salida:

Presione enter para continuar
Error: se produjeron uno o más errores. Espere Task.Delay
Error: segundo

Tenga en cuenta que tuve que presionar Enterpara verlo.

Ahora, si llamo DoTestAsync(AnotherTestAsync, -2), el flujo de trabajo del código interno DoTestAsynces bastante diferente, al igual que la salida. Esta vez, no me pidieron que presione Enter:

Error: el valor debe ser -1 (que significa un tiempo de espera infinito), 0 o un número entero positivo.
Nombre del parámetro: milisegundosDelayError: 1st

En ambos casos se Task.Delay(-2)lanza al principio, mientras valida sus parámetros. Este podría ser un escenario inventado, pero en teoría Task.Delay(1000)también puede producirse , por ejemplo, cuando falla la API del temporizador del sistema subyacente.

En una nota al margen, la lógica de propagación de errores es aún diferente para los async voidmétodos (a diferencia de los async Taskmétodos). Una excepción generada dentro de un async voidmétodo se volverá a lanzar inmediatamente en el contexto de sincronización del hilo actual (vía SynchronizationContext.Post), si el hilo actual tiene uno ( SynchronizationContext.Current != null). De lo contrario, se volverá a lanzar vía ThreadPool.QueueUserWorkItem). La persona que llama no tiene la oportunidad de manejar esta excepción en el mismo marco de pila.

Publiqué algunos detalles más sobre el comportamiento de manejo de excepciones de TPL aquí y aquí .


P : ¿Es posible imitar el comportamiento de propagación de excepciones de los asyncmétodos para métodos no Taskbasados ​​en asíncronos , de modo que este último no se ejecute en el mismo marco de pila?

R : Si realmente es necesario, entonces sí, hay un truco para eso:

// async
async Task<int> MethodAsync(int arg)
{
    if (arg < 0)
        throw new ArgumentException("arg");
    // ...
    return 42 + arg;
}

// non-async
Task<int> MethodAsync(int arg)
{
    var task = new Task<int>(() => 
    {
        if (arg < 0)
            throw new ArgumentException("arg");
        // ...
        return 42 + arg;
    });

    task.RunSynchronously(TaskScheduler.Default);
    return task;
}

Sin embargo, tenga en cuenta que bajo ciertas condiciones (como cuando está demasiado profundo en la pila), RunSynchronouslyaún podría ejecutarse de forma asincrónica.


Otra diferencia notable es que la versión async/ awaites más propensa a bloquearse en un contexto de sincronización no predeterminado . Por ejemplo, lo siguiente se bloqueará en una aplicación WinForms o WPF:

static async Task TestAsync()
{
    await Task.Delay(1000);
}

void Form_Load(object sender, EventArgs e)
{
    TestAsync().Wait(); // dead-lock here
}

Cámbielo a una versión no asíncrona y no se bloqueará:

Task TestAsync() 
{
    return Task.Delay(1000);
}

La naturaleza del bloqueo está bien explicada por Stephen Cleary en su blog .

ratio nasal
fuente
2
Creo que el punto muerto en el primer ejemplo podría evitarse agregando .ConfigureAwait (false) a la línea de espera, ya que solo sucede porque el método está tratando de regresar al mismo contexto de ejecución. Entonces las excepciones son la única diferencia que queda.
relativamente_random
2
@relatively_random, su comentario es correcto, aunque la respuesta fue sobre la diferencia entre return Task.Run()y await Task.Run(); return, en lugar deawait Task.Run().ConfigureAwait(false); return
noseratio
Si encuentra que el programa se cierra después de presionar Enter, asegúrese de presionar ctrl + F5 en lugar de F5.
David Klempfner
54

Cuál es la diferencia entre

async Task TestAsync() 
{
    await Task.Delay(1000);
}

y

Task TestAsync() 
{
    return Task.Delay(1000);
}

?

Estoy confundido por esta pregunta. Permítame intentar aclararlo respondiendo a su pregunta con otra pregunta. ¿Cuál es la diferencia entre?

Func<int> MakeFunction()
{
    Func<int> f = ()=>1;
    return ()=>f();
}

y

Func<int> MakeFunction()
{
    return ()=>1;
}

?

Cualquiera que sea la diferencia entre mis dos cosas, la misma diferencia existe entre tus dos cosas.

Eric Lippert
fuente
23
¡Por supuesto! Has abierto mis ojos :) En el primer caso, creo una tarea contenedora, semánticamente cercana a Task.Delay(1000).ContinueWith(() = {}). En el segundo, es solo Task.Delay(1000). La diferencia es algo sutil, pero significativa.
avo
3
¿Podrías explicar un poco la diferencia? en realidad no ... Gracias
zheng yu
4
Dado que hay una diferencia sutil con los contextos de sincronización, y la propagación de excepciones, diría que la diferencia entre los envoltorios async / await y function no es la misma.
Cameron MacFarland
1
@CameronMacFarland: Por eso pedí una aclaración. La pregunta pregunta si existe una diferencia conceptual entre los dos. Bueno, no lo se. Ciertamente hay muchas diferencias; ¿Alguno de ellos cuenta como diferencias "conceptuales"? En mi ejemplo con funciones anidadas, también hay diferencias en la propagación de errores; si las funciones están cerradas sobre el estado local, hay diferencias en la vida útil local, etc. ¿Son estas diferencias "conceptuales"?
Eric Lippert
6
Esta es una respuesta antigua, pero creo que dada hoy, habría sido rechazada. No responde a la pregunta, ni apunta al OP a una fuente de la que pueda aprender.
Daniel Dubovski
11
  1. El primer método ni siquiera se compila.

    Dado que ' Program.TestAsync()' es un método asíncrono que devuelve ' Task', una palabra clave de retorno no debe ir seguida de una expresión de objeto. ¿Tenías la intención de volver ' Task<T>'?

    Tiene que ser

    async Task TestAsync()
    {
        await Task.Run(() => DoSomeWork());
    }
    
  2. Existe una gran diferencia conceptual entre estos dos. El primero es asincrónico, el segundo no lo es. Lea Async Performance: Understanding the Costs of Async y Await para obtener más información sobre los aspectos internos de async/ await.

  3. Generan código diferente.

    .method private hidebysig 
        instance class [mscorlib]System.Threading.Tasks.Task TestAsync () cil managed 
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
            01 00 25 53 4f 54 65 73 74 50 72 6f 6a 65 63 74
            2e 50 72 6f 67 72 61 6d 2b 3c 54 65 73 74 41 73
            79 6e 63 3e 64 5f 5f 31 00 00
        )
        .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x216c
        // Code size 62 (0x3e)
        .maxstack 2
        .locals init (
            [0] valuetype SOTestProject.Program/'<TestAsync>d__1',
            [1] class [mscorlib]System.Threading.Tasks.Task,
            [2] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder
        )
    
        IL_0000: ldloca.s 0
        IL_0002: ldarg.0
        IL_0003: stfld class SOTestProject.Program SOTestProject.Program/'<TestAsync>d__1'::'<>4__this'
        IL_0008: ldloca.s 0
        IL_000a: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()
        IL_000f: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0014: ldloca.s 0
        IL_0016: ldc.i4.m1
        IL_0017: stfld int32 SOTestProject.Program/'<TestAsync>d__1'::'<>1__state'
        IL_001c: ldloca.s 0
        IL_001e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0023: stloc.2
        IL_0024: ldloca.s 2
        IL_0026: ldloca.s 0
        IL_0028: call instance void [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<valuetype SOTestProject.Program/'<TestAsync>d__1'>(!!0&)
        IL_002d: ldloca.s 0
        IL_002f: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0034: call instance class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()
        IL_0039: stloc.1
        IL_003a: br.s IL_003c
    
        IL_003c: ldloc.1
        IL_003d: ret
    } // end of method Program::TestAsync
    

    y

    .method private hidebysig 
        instance class [mscorlib]System.Threading.Tasks.Task TestAsync2 () cil managed 
    {
        // Method begins at RVA 0x21d8
        // Code size 23 (0x17)
        .maxstack 2
        .locals init (
            [0] class [mscorlib]System.Threading.Tasks.Task CS$1$0000
        )
    
        IL_0000: nop
        IL_0001: ldarg.0
        IL_0002: ldftn instance class [mscorlib]System.Threading.Tasks.Task SOTestProject.Program::'<TestAsync2>b__4'()
        IL_0008: newobj instance void class [mscorlib]System.Func`1<class [mscorlib]System.Threading.Tasks.Task>::.ctor(object, native int)
        IL_000d: call class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Run(class [mscorlib]System.Func`1<class [mscorlib]System.Threading.Tasks.Task>)
        IL_0012: stloc.0
        IL_0013: br.s IL_0015
    
        IL_0015: ldloc.0
        IL_0016: ret
    } // end of method Program::TestAsync2
    
MarcinJuraszek
fuente
@MarcinJuraszek, de hecho no se compiló. Fue un error tipográfico, estoy seguro de que lo hizo bien. De lo contrario, una gran respuesta, ¡gracias! Pensé que C # podría ser lo suficientemente inteligente como para evitar generar una clase de máquina de estado en el primer caso.
avo
9

Los dos ejemplos no difieren. Cuando un método está marcado con la asyncpalabra clave, el compilador genera una máquina de estado detrás de escena. Esto es lo que se encarga de reanudar las continuaciones una vez que se ha esperado una esperada.

Por el contrario, cuando un método no está marcado con async, se pierde la capacidad de awaitesperar. (Es decir, dentro del método en sí; la persona que llama aún puede esperar el método). Sin embargo, al evitar la asyncpalabra clave, ya no está generando la máquina de estado, que puede agregar un poco de sobrecarga (elevando locales a los campos de la máquina de estado, objetos adicionales al GC).

En ejemplos como este, si puede evitar async-awaity devolver un awaitable directamente, debe hacerlo para mejorar la eficiencia del método.

Vea esta pregunta y esta respuesta que son muy similares a su pregunta y esta respuesta.

Lukazoide
fuente