¿Debo preocuparme por "Este método asíncrono carece de operadores 'en espera' y se ejecutará sincrónicamente"?

93

Tengo una interfaz que expone algunos métodos asincrónicos. Más específicamente, tiene métodos definidos que devuelven Task o Task <T>. Estoy usando las palabras clave async / await.

Estoy en el proceso de implementar esta interfaz. Sin embargo, en algunos de estos métodos, esta implementación no tiene nada que esperar. Por esa razón, recibo la advertencia del compilador "Este método asincrónico carece de operadores 'en espera' y se ejecutará sincrónicamente ..."

Entiendo por qué recibo el error, pero me pregunto si debería hacer algo al respecto en este contexto. Se siente mal ignorar las advertencias del compilador.

Sé que puedo solucionarlo esperando en Task.Run, pero eso se siente mal para un método que solo realiza algunas operaciones económicas. También parece que agregará una sobrecarga innecesaria a la ejecución, pero tampoco estoy seguro de si eso ya está allí porque la palabra clave async está presente.

¿Debo simplemente ignorar las advertencias o hay alguna forma de evitar esto que no veo?

dannykay1710
fuente
2
Dependerá de los detalles. ¿Está realmente seguro de que desea que estas operaciones se realicen sincrónicamente? Si desea que se realicen de forma sincrónica, ¿por qué el método está marcado como async?
Servicio
11
Simplemente elimine la asyncpalabra clave. Todavía puede devolver un Taskusing Task.FromResult.
Michael Liu
1
@BenVoigt Google está lleno de información al respecto, en caso de que el OP aún no lo sepa.
Servicio
1
@BenVoigt ¿No dio ya Michael Liu esa pista? Utilice Task.FromResult.
1
@hvd: Eso fue editado en su comentario más tarde.
Ben Voigt

Respuestas:

144

La palabra clave async es simplemente un detalle de implementación de un método; no es parte de la firma del método. Si la implementación o anulación de un método en particular no tiene nada que esperar, simplemente omita la palabra clave async y devuelva una tarea completada usando Task.FromResult <TResult> :

public Task<string> Foo()               //    public async Task<string> Foo()
{                                       //    {
    Baz();                              //        Baz();
    return Task.FromResult("Hello");    //        return "Hello";
}                                       //    }

Si su método devuelve Task en lugar de Task <TResult> , puede devolver una tarea completada de cualquier tipo y valor. Task.FromResult(0)parece ser una opción popular:

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.FromResult(0);          //
}                                       //    }

O, a partir de .NET Framework 4.6, puede devolver Task.CompletedTask :

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.CompletedTask;          //
}                                       //    }
Michael Liu
fuente
Gracias, creo que lo que me faltaba era el concepto de crear una tarea que se completó, en lugar de devolver una tarea real que, como dices, sería lo mismo que tener la palabra clave async. Parece obvio ahora, ¡pero no lo estaba viendo!
dannykay1710
1
Task podría funcionar con un miembro estático similar a Task.Empty para este propósito. La intención sería un poco más clara y me duele pensar en todas estas Tareas obedientes que devuelven un cero que nunca se necesita.
Rupert Rawnsley
await Task.FromResult(0)? ¿Qué tal await Task.Yield()?
Sushi271
1
@ Sushi271: No, si no es un asyncmétodo, regresa en Task.FromResult(0) lugar de esperarlo.
Michael Liu
1
En realidad NO, async no es solo un detalle de implementación, hay muchos detalles que uno debe tener en cuenta :). Hay que saber qué parte se ejecuta de forma sincrónica, qué parte de forma asincrónica, cuál es el contexto de sincronización actual y solo para el registro, las tareas son siempre un poco más rápidas, ya que no hay una máquina de estado detrás de las cortinas :)
ipavlu
16

Es perfectamente razonable que algunas operaciones "asincrónicas" se completen sincrónicamente, pero aún así se ajusten al modelo de llamadas asincrónicas por el polimorfismo.

Un ejemplo real de esto es con las API de E / S del SO. Las llamadas asincrónicas y superpuestas en algunos dispositivos siempre se completan en línea (la escritura en una tubería implementada mediante memoria compartida, por ejemplo). Pero implementan la misma interfaz que las operaciones de varias partes que continúan en segundo plano.

Ben Voigt
fuente
4

Michael Liu respondió bien a su pregunta sobre cómo puede evitar la advertencia: devolviendo Task.FromResult.

Voy a responder a la parte de su pregunta "¿Debería preocuparme por la advertencia?".

¡La respuesta es sí!

La razón de esto es que la advertencia aparece con frecuencia cuando llama a un método que regresa Taskdentro de un método asíncrono sin el awaitoperador. Acabo de arreglar un error de concurrencia que sucedió porque invoqué una operación en Entity Framework sin esperar la operación anterior.

Si puede escribir meticulosamente su código para evitar las advertencias del compilador, cuando haya una advertencia, se destacará como un pulgar adolorido. Podría haber evitado varias horas de depuración.

Río vivian
fuente
5
Esta respuesta es simplemente incorrecta. He aquí por qué: puede haber al menos uno awaitdentro del método en un lugar (no habrá CS1998) pero no significa que no habrá otra llamada al método asnyc que carezca de sincronización (usando awaito cualquier otro). Ahora, si alguien quisiera saber cómo asegurarse de que no se pierda la sincronización accidentalmente, asegúrese de no ignorar otra advertencia: CS4014. Incluso recomendaría amenazarlo como error.
Victor Yarema
3

Puede que sea demasiado tarde, pero podría ser útil una investigación:

Hay una estructura interna del código compilado ( IL ):

 public static async Task<int> GetTestData()
    {
        return 12;
    }

se convierte en en IL:

.method private hidebysig static class [mscorlib]System.Threading.Tasks.Task`1<int32> 
        GetTestData() cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 28 55 73 61 67 65 4C 69 62 72 61 72 79 2E   // ..(UsageLibrary.
                                                                                                                                     53 74 61 72 74 54 79 70 65 2B 3C 47 65 74 54 65   // StartType+<GetTe
                                                                                                                                     73 74 44 61 74 61 3E 64 5F 5F 31 00 00 )          // stData>d__1..
  .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       52 (0x34)
  .maxstack  2
  .locals init ([0] class UsageLibrary.StartType/'<GetTestData>d__1' V_0,
           [1] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> V_1)
  IL_0000:  newobj     instance void UsageLibrary.StartType/'<GetTestData>d__1'::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  call       valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Create()
  IL_000c:  stfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_0011:  ldloc.0
  IL_0012:  ldc.i4.m1
  IL_0013:  stfld      int32 UsageLibrary.StartType/'<GetTestData>d__1'::'<>1__state'
  IL_0018:  ldloc.0
  IL_0019:  ldfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_001e:  stloc.1
  IL_001f:  ldloca.s   V_1
  IL_0021:  ldloca.s   V_0
  IL_0023:  call       instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Start<class UsageLibrary.StartType/'<GetTestData>d__1'>(!!0&)
  IL_0028:  ldloc.0
  IL_0029:  ldflda     valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_002e:  call       instance class [mscorlib]System.Threading.Tasks.Task`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::get_Task()
  IL_0033:  ret
} // end of method StartType::GetTestData

Y sin método asincrónico y de tarea:

 public static int GetTestData()
        {
            return 12;
        }

se convierte en:

.method private hidebysig static int32  GetTestData() cil managed
{
  // Code size       8 (0x8)
  .maxstack  1
  .locals init ([0] int32 V_0)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   12
  IL_0003:  stloc.0
  IL_0004:  br.s       IL_0006
  IL_0006:  ldloc.0
  IL_0007:  ret
} // end of method StartType::GetTestData

Como puede ver, la gran diferencia entre estos métodos. Si no usa el método await inside async y no le importa usar el método async (por ejemplo, una llamada a la API o un controlador de eventos), la buena idea lo convertirá al método de sincronización normal (ahorra el rendimiento de su aplicación).

Actualizado:

También hay información adicional de microsoft docs https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth :

Los métodos asíncronos deben tener una palabra clave de espera en su cuerpo o nunca cederán. Es importante tener esto en cuenta. Si await no se usa en el cuerpo de un método asíncrono, el compilador de C # generará una advertencia, pero el código se compilará y ejecutará como si fuera un método normal. Tenga en cuenta que esto también sería increíblemente ineficiente, ya que la máquina de estado generada por el compilador de C # para el método asíncrono no lograría nada.

Oleg Bondarenko
fuente
2
Además, su conclusión final sobre el uso de async/awaitestá muy simplificada, ya que la basa en su ejemplo poco realista de una sola operación que está vinculada a la CPU. Tasks cuando se usa correctamente permite mejorar el rendimiento y la capacidad de respuesta de la aplicación debido a tareas concurrentes (es decir, en paralelo) y una mejor administración y uso de subprocesos
MickyD
Eso es solo un ejemplo simplificado de prueba como dije en esta publicación. También mencioné sobre solicitudes a api y event hendlers donde sea posible usando ambas versiones de métodos (async y regular). También PO dijo sobre el uso de métodos asíncronos sin esperar adentro. Mi publicación fue sobre eso, pero no sobre el uso adecuado Tasks. Es triste que no estés leyendo el texto completo de la publicación y sacando conclusiones rápidamente.
Oleg Bondarenko
1
Hay una diferencia entre un método que devuelve int(como en su caso) y uno que devuelve Taskcomo lo discutió el OP. Lea su publicación y la respuesta aceptada nuevamente en lugar de tomarse las cosas personalmente. Su respuesta no es útil en este caso. Ni siquiera te molestas en mostrar la diferencia entre un método que tiene awaitdentro o no. Ahora que había hecho que eso habría sido muy bueno bien vale la pena una upvote
MickyD
Supongo que realmente no entiendes la diferencia entre el método asíncrono y los regulares que se llaman con api o controladores de eventos. Se mencionó especialmente en mi publicación. Lamento que te lo pierdas de nuevo .
Oleg Bondarenko
1

Nota sobre el comportamiento de excepción al regresar Task.FromResult

Aquí hay una pequeña demostración que muestra la diferencia en el manejo de excepciones entre los métodos marcados y no marcados con async.

public Task<string> GetToken1WithoutAsync() => throw new Exception("Ex1!");

// Warning: This async method lacks 'await' operators and will run synchronously. Consider ...
public async Task<string> GetToken2WithAsync() => throw new Exception("Ex2!");  

public string GetToken3Throws() => throw new Exception("Ex3!");
public async Task<string> GetToken3WithAsync() => await Task.Run(GetToken3Throws);

public async Task<string> GetToken4WithAsync() { throw new Exception("Ex4!"); return await Task.FromResult("X");} 


public static async Task Main(string[] args)
{
    var p = new Program();

    try { var task1 = p.GetToken1WithoutAsync(); } 
    catch( Exception ) { Console.WriteLine("Throws before await.");};

    var task2 = p.GetToken2WithAsync(); // Does not throw;
    try { var token2 = await task2; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};

    var task3 = p.GetToken3WithAsync(); // Does not throw;
    try { var token3 = await task3; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};

    var task4 = p.GetToken4WithAsync(); // Does not throw;
    try { var token4 = await task4; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};
}
// .NETCoreApp,Version=v3.0
Throws before await.
Throws on await.
Throws on await.
Throws on await.

(Publicación cruzada de mi respuesta para When async Task <T> required by interface, how to get return variable without compiler warning )

tymtam
fuente