Devo me preocupar com o aviso “Este método assíncrono não possui operadores 'em espera' e será executado de forma síncrona”

95

Tenho uma interface que expõe alguns métodos assíncronos. Mais especificamente, ele possui métodos definidos que retornam Task ou Task <T>. Estou usando as palavras-chave async / await.

Estou implementando esta interface. No entanto, em alguns desses métodos, essa implementação não tem nada a esperar. Por esse motivo, estou recebendo o aviso do compilador "Este método assíncrono não tem operadores 'em espera' e será executado de forma síncrona ..."

Eu entendo por que estou recebendo o erro, mas me pergunto se devo fazer algo a respeito neste contexto. Parece errado ignorar os avisos do compilador.

Eu sei que posso consertá-lo aguardando em um Task.Run, mas isso parece errado para um método que faz apenas algumas operações baratas. Também parece que vai adicionar sobrecarga desnecessária à execução, mas também não tenho certeza se isso já está lá porque a palavra-chave async está presente.

Devo simplesmente ignorar os avisos ou há uma maneira de contornar isso que não estou vendo?

dannykay1710
fonte
2
Vai depender dos detalhes. Tem certeza de que deseja que essas operações sejam realizadas de forma síncrona? Se você deseja que sejam executados de forma síncrona, por que o método está marcado como async?
Servy
11
Basta remover a asyncpalavra - chave. Você ainda pode devolver um Taskusando Task.FromResult.
Michael Liu
1
@BenVoigt O Google está repleto de informações sobre isso, caso o OP ainda não saiba.
Servy
1
@BenVoigt Michael Liu já não deu essa dica? Use Task.FromResult.
1
@hvd: Isso foi editado em seu comentário mais tarde.
Ben Voigt

Respostas:

147

A palavra - chave async é apenas um detalhe de implementação de um método; não faz parte da assinatura do método. Se a implementação ou substituição de um método específico não tiver nada a esperar, basta omitir a palavra - chave async e retornar uma tarefa concluída usando Task.FromResult <TResult> :

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

Se o seu método retornar Task em vez de Task <TResult> , você poderá retornar uma tarefa concluída de qualquer tipo e valor. Task.FromResult(0)parece ser uma escolha popular:

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

Ou, a partir do .NET Framework 4.6, você pode retornar Task.CompletedTask :

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.CompletedTask;          //
}                                       //    }
Michael Liu
fonte
Obrigado, acho que o que estava faltando era o conceito de criar uma tarefa que foi concluída, em vez de retornar uma tarefa real que, como você diz, seria o mesmo que ter a palavra-chave async. Parece óbvio agora, mas eu simplesmente não estava vendo!
dannykay1710
1
A tarefa poderia ser feita com um membro estático ao longo das linhas de Task.Empty para essa finalidade. A intenção seria um pouco mais clara e me dói pensar em todas essas tarefas zelosas que retornam um zero que nunca é necessário.
Rupert Rawnsley,
await Task.FromResult(0)? Que tal await Task.Yield()?
Sushi271
1
@ Sushi271: Não, em um não asyncmétodo, você retorna ao Task.FromResult(0) invés de esperar.
Michael Liu
1
Na verdade NÃO, async não é apenas um detalhe de implementação, existem muitos detalhes sobre os quais você deve estar ciente :). É preciso estar atento, qual parte roda de forma síncrona, qual parte de forma assíncrona, qual é o contexto de sincronização atual e apenas para registro, as tarefas são sempre um pouco mais rápidas, pois não há máquina de estados atrás das cortinas :).
ipavlu,
17

É perfeitamente razoável que algumas operações "assíncronas" sejam concluídas de forma síncrona, embora ainda estejam em conformidade com o modelo de chamada assíncrona por causa do polimorfismo.

Um exemplo real disso é com as APIs de E / S do sistema operacional. As chamadas assíncronas e sobrepostas em alguns dispositivos sempre são concluídas inline (gravação em um pipe implementado usando memória compartilhada, por exemplo). Mas eles implementam a mesma interface que as operações de várias partes que continuam em segundo plano.

Ben Voigt
fonte
4

Michael Liu respondeu bem à sua pergunta sobre como você pode evitar o aviso: retornando Task.FromResult.

Vou responder à parte "Devo me preocupar com o aviso" da sua pergunta.

A resposta é sim!

A razão para isso é que o aviso freqüentemente ocorre quando você chama um método que retorna Taskdentro de um método assíncrono sem o awaitoperador. Acabei de corrigir um bug de simultaneidade que acontecia porque invoquei uma operação no Entity Framework sem esperar a operação anterior.

Se você puder escrever seu código meticulosamente para evitar avisos do compilador, então quando houver um aviso, ele se destacará como um polegar dolorido. Eu poderia ter evitado várias horas de depuração.

Rio Vivian
fonte
5
Esta resposta está simplesmente errada. Aqui está o porquê: pode haver pelo menos um awaitdentro do método em um lugar (não haverá CS1998), mas isso não significa que não haverá nenhuma outra chamada do método asnyc que não terá a sincronização (usando awaitou qualquer outro). Agora, se alguém quiser saber como ter certeza de que você não perderá a sincronização acidentalmente, certifique-se de não ignorar outro aviso - CS4014. Eu até recomendaria ameaçar isso como um erro.
Victor Yarema
4

Pode ser tarde demais, mas pode ser uma investigação útil:

Há sobre a estrutura interna do código compilado ( IL ):

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

torna-se em 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

E sem método assíncrono e tarefa:

 public static int GetTestData()
        {
            return 12;
        }

torna-se :

.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 você pode ver a grande diferença entre esses métodos. Se você não usar await dentro do método assíncrono e não se preocupa em usar o método assíncrono (por exemplo, chamada de API ou manipulador de eventos), a boa ideia irá convertê-lo para o método de sincronização normal (ele salva o desempenho do seu aplicativo).

Atualizada:

Também há informações adicionais nos documentos da microsoft https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth :

os métodos assíncronos precisam ter uma palavra-chave await em seu corpo ou eles nunca irão render! É importante ter isso em mente. Se await não for usado no corpo de um método assíncrono, o compilador C # gerará um aviso, mas o código será compilado e executado como se fosse um método normal. Observe que isso também seria incrivelmente ineficiente, pois a máquina de estado gerada pelo compilador C # para o método assíncrono não realizaria nada.

Oleg Bondarenko
fonte
2
Além disso, sua conclusão final sobre o uso de async/awaité extremamente simplificada, pois você a está baseando em seu exemplo irreal de uma única operação que é limitada pela CPU. Tasks, quando usado corretamente, permite melhor desempenho do aplicativo e capacidade de resposta devido a tarefas simultâneas (ou seja, paralelas) e melhor gerenciamento e uso de threads
MickyD
Isso é apenas um exemplo de teste simplificado como falei neste post. Também mencionei sobre as solicitações para api e hendlers de evento onde possível usando ambas as versões dos métodos (assíncrono e regular). PO também disse sobre o uso de métodos assíncronos sem esperar dentro. Minha postagem era sobre isso, mas não sobre o uso adequado Tasks. É uma história triste que você não esteja lendo o texto inteiro do post e tirando conclusões rapidamente.
Oleg Bondarenko de
2
Há uma diferença entre um método que retorna int(como no seu caso) e um que retorna Taskcomo discutido pelo OP. Leia a postagem e a resposta aceita novamente, em vez de levar as coisas para o lado pessoal. Sua resposta não é útil neste caso. Você nem se preocupa em mostrar a diferença entre um método que tem awaitdentro ou não. Agora você tivesse feito isso, que teria sido muito bom vale a pena uma upvote
MickyD
Eu acho que você realmente não entende a diferença entre o método assíncrono e os regulares que são chamados com api ou manipuladores de eventos. Foi especialmente mencionado na minha postagem. Desculpe por você estar perdendo isso de novo .
Oleg Bondarenko de
1

Nota sobre o comportamento da exceção ao retornar Task.FromResult

Aqui está uma pequena demonstração que mostra a diferença no tratamento de exceções entre métodos marcados e não marcados com 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.

(Postagem cruzada da minha resposta para Quando a tarefa assíncrona <T> exigida pela interface, como obter a variável de retorno sem aviso do compilador )

tymtam
fonte