'wait' funciona, mas chama a tarefa. Resultado trava / deadlocks

126

Eu tenho os quatro testes a seguir e o último trava quando eu o executo. Por que isso acontece:

[Test]
public void CheckOnceResultTest()
{
    Assert.IsTrue(CheckStatus().Result);
}

[Test]
public async void CheckOnceAwaitTest()
{
    Assert.IsTrue(await CheckStatus());
}

[Test]
public async void CheckStatusTwiceAwaitTest()
{
    Assert.IsTrue(await CheckStatus());
    Assert.IsTrue(await CheckStatus());
}

[Test]
public async void CheckStatusTwiceResultTest()
{
    Assert.IsTrue(CheckStatus().Result); // This hangs
    Assert.IsTrue(await CheckStatus());
}

private async Task<bool> CheckStatus()
{
    var restClient = new RestClient(@"https://api.test.nordnet.se/next/1");
    Task<IRestResponse<DummyServiceStatus>> restResponse = restClient.ExecuteTaskAsync<DummyServiceStatus>(new RestRequest(Method.GET));
    IRestResponse<DummyServiceStatus> response = await restResponse;
    return response.Data.SystemRunning;
}

Eu uso esse método de extensão para o Restsharp RestClient :

public static class RestClientExt
{
    public static Task<IRestResponse<T>> ExecuteTaskAsync<T>(this RestClient client, IRestRequest request) where T : new()
    {
        var tcs = new TaskCompletionSource<IRestResponse<T>>();
        RestRequestAsyncHandle asyncHandle = client.ExecuteAsync<T>(request, tcs.SetResult);
        return tcs.Task;
    }
}
public class DummyServiceStatus
{
    public string Message { get; set; }
    public bool ValidVersion { get; set; }
    public bool SystemRunning { get; set; }
    public bool SkipPhrase { get; set; }
    public long Timestamp { get; set; }
}

Por que o último teste travou?

Johan Larsson
fonte
7
Você deve evitar retornar nulos de métodos assíncronos. É apenas para compatibilidade com versões anteriores com manipuladores de eventos existentes, principalmente em código de interface. Se o seu método assíncrono não retornar nada, ele deverá retornar a tarefa. Eu tive vários problemas com o MSTest e anulei os testes assíncronos.
Ghord
2
@ghord: O MSTest não suporta async voidmétodos de teste de unidade; eles simplesmente não funcionam. No entanto, o NUnit faz. Dito isto, estou de acordo com o princípio geral de preferir async Taskmais async void.
Stephen Cleary
@StephenCleary Sim, apesar de ter sido permitido no betas do VS2012, o que estava causando todos os tipos de problemas.
Ghord

Respostas:

88

Você está enfrentando a situação padrão de conflito que eu descrevo no meu blog e em um artigo do MSDN : o asyncmétodo está tentando agendar sua continuação em um segmento que está sendo bloqueado pela chamada para Result.

Nesse caso, SynchronizationContexté o seu usado pelo NUnit para executar async voidmétodos de teste. Eu tentaria usar async Taskmétodos de teste.

Stephen Cleary
fonte
4
mudando para assíncrona Tarefa funcionou, agora preciso ler o conteúdo de seus links algumas vezes, senhor.
Johan Larsson
@ MarioLopez: A solução é usar " asynctodo o caminho" (conforme observado no meu artigo do MSDN). Em outras palavras - como diz o título da minha postagem no blog - "não bloqueie no código assíncrono".
Stephen Cleary
1
@StephenCleary, e se eu tiver que chamar um método assíncrono dentro de um construtor? Os construtores não podem ser assíncronos.
Raikol Amaro 03/08/19
1
@StephenCleary Em quase todas as suas respostas no SO e nos seus artigos, tudo o que eu já vi falar foi sobre substituir Wait()o método de chamada async. Mas para mim, isso parece estar levando o problema a montante. Em algum momento, algo precisa ser gerenciado de forma síncrona. E se minha função for propositalmente síncrona porque gerencia com threads de trabalho de longa execução Task.Run()? Como espero que isso termine sem conflito dentro do meu teste NUnit?
void.pointer
1
@ void.pointer: At some point, something has to be managed synchronously.- de maneira alguma. Para aplicativos de interface do usuário, o ponto de entrada pode ser um async voidmanipulador de eventos. Para aplicativos de servidor, o ponto de entrada pode ser uma async Task<T>ação. É preferível usar os asyncdois para evitar o bloqueio de threads. Você pode fazer com que seu teste NUnit seja síncrono ou assíncrono; se assíncrono, faça-o em async Taskvez de async void. Se for síncrono, não deve ter um, SynchronizationContextentão não deve haver um impasse.
Stephen Cleary
222

Adquirindo um valor por meio de um método assíncrono:

var result = Task.Run(() => asyncGetValue()).Result;

Chamar um método assíncrono de forma síncrona

Task.Run( () => asyncMethod()).Wait();

Nenhum problema de conflito ocorrerá devido ao uso do Task.Run.

Herman Schoenfeld
fonte
15
-1 para incentivar o uso de async voidmétodos de teste de unidade e remover as garantias de mesma rosca fornecidas pelo SynchronizationContextsistema do teste.
Stephen Cleary
68
@StephenCleary: não há "encorajador" de vácuo assíncrono. É apenas o emprego de uma construção c # válida para resolver o problema do impasse. O snippet acima é uma solução indispensável e simples para a questão do OP. O Stackoverflow trata de soluções para problemas, não de autopromoção detalhada.
Herman Schoenfeld
81
@StephenCleary: seus artigos realmente não articulam a solução (pelo menos não claramente) e, mesmo que você tivesse uma solução, usaria essas construções indiretamente. Minha solução não usa contextos explicitamente, e daí? O ponto é que o meu funciona e é uma linha. Não precisava de duas postagens no blog e milhares de palavras para resolver o problema. NOTA: Eu nem uso o async void , por isso não sei realmente do que você está falando. Você vê "async void" em algum lugar da minha resposta concisa e adequada?
Herman Schoenfeld
15
@HermanSchoenfeld, se você adicionou o porquê ao como , acredito que sua resposta beneficiaria muito.
precisa saber é o seguinte
19
Eu sei que isto é uma espécie de tarde, mas você deve estar usando .GetAwaiter().GetResult()em vez de .Resultmodo que qualquer Exceptionnão está embrulhado.
Camilo Terevinto
15

Você pode evitar o impasse adicionando ConfigureAwait(false)a esta linha:

IRestResponse<DummyServiceStatus> response = await restResponse;

=>

IRestResponse<DummyServiceStatus> response = await restResponse.ConfigureAwait(false);

Eu descrevi essa armadilha no meu blog Pitfalls of async / waitit

Vladimir
fonte
9

Você está bloqueando a interface do usuário usando a propriedade Task.Result. Na documentação do MSDN, eles mencionaram claramente que,

"A propriedade Result é uma propriedade de bloqueio. Se você tentar acessá-la antes que sua tarefa seja concluída, o encadeamento atualmente ativo será bloqueado até que a tarefa seja concluída e o valor esteja disponível. Na maioria dos casos, você deve acessar o valor usando Aguardar ou aguarde em vez de acessar a propriedade diretamente ".

A melhor solução para esse cenário seria remover os modos de espera e assíncrona e usar apenas a Tarefa em que você está retornando o resultado. Não vai estragar sua sequência de execução.

Cavaleiro das Trevas
fonte
3

Se você não receber retornos de chamada ou o controle desligar, depois de chamar a função de serviço / API assíncrona, será necessário configurar o Contexto para retornar um resultado no mesmo contexto chamado.

Usar TestAsync().ConfigureAwait(continueOnCapturedContext: false);

Você enfrentará esse problema apenas em aplicativos da Web, mas não em static void main.

Mayank Pandit
fonte
ConfigureAwaitevita o impasse em determinados cenários por não ser executado no contexto do encadeamento original.
Davidcarr