Chamando o método assíncrono de forma síncrona


231

Eu tenho um asyncmétodo:

public async Task<string> GenerateCodeAsync()
{
    string code = await GenerateCodeService.GenerateCodeAsync();
    return code;
}

Eu preciso chamar esse método a partir de um método síncrono.

Como fazer isso sem precisar duplicar o GenerateCodeAsyncmétodo para que ele funcione de forma síncrona?

Atualizar

No entanto, nenhuma solução razoável foi encontrada.

No entanto, vejo que HttpClientjá implementa esse padrão

using (HttpClient client = new HttpClient())
{
    // async
    HttpResponseMessage responseAsync = await client.GetAsync(url);

    // sync
    HttpResponseMessage responseSync = client.GetAsync(url).Result;
}


1
Eu estava esperando por uma solução mais simples, pensando que asp.net lidou com isso muito mais fácil do que escrever tantas linhas de código
— Catalin

Por que não adotar apenas código assíncrono? Idealmente, você deseja mais código assíncrono, não menos.
— Paulo Morgado

54
[Por que não adotar apenas código assíncrono?] Ha, pode ser precisamente porque alguém está adotando código assíncrono que eles precisam dessa solução, pois grandes partes do projeto são convertidas! Você não pode reconstruir Roma em um dia.
— Nicholas Petersen

1
Às vezes, a biblioteca de terceiros @NicholasPetersen pode forçá-lo a fazer isso. Exemplo de criação de mensagens dinâmicas no método WithMessage fora do FluentValidation. Não há API assíncrona para isso devido ao design da biblioteca - as sobrecargas do WithMessage são estáticas. Outros métodos de passar argumentos dinâmicos para WithMessage são estranhos.
— H.Wojtowicz

Respostas:


278

Você pode acessar a Resultpropriedade da tarefa, que fará com que seu encadeamento seja bloqueado até que o resultado esteja disponível:

string code = GenerateCodeAsync().Result;

Nota: Em alguns casos, isso pode levar a um impasse: Sua chamada para Resultbloquear o encadeamento principal, impedindo a execução do restante do código assíncrono. Você tem as seguintes opções para garantir que isso não aconteça:

Isso não significa que você deve adicionar sem pensar .ConfigureAwait(false)depois de todas as chamadas assíncronas! Para uma análise detalhada sobre por que e quando você deve usar .ConfigureAwait(false), consulte a seguinte postagem no blog:


33
Se invocar resultum conflito, então quando é seguro obter o resultado? Toda chamada assíncrona exige Task.Runou ConfigureAwait(false)?
— 9788 Robert Harvey

4
Não existe um "thread principal" em ASP.NET (ao contrário de um aplicativo GUI), mas o impasse ainda é possível por causa da forma como AspNetSynchronizationContext.Post Serializa assíncrona continuações:Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action)); _lastScheduledTask = newTask;
— noseratio

4
@RobertHarvey: Se você não tem controle sobre a implementação do método assíncrono que está bloqueando, sim, deve envolvê-lo Task.Runpara se manter seguro. Ou use algo como WithNoContextpara reduzir a troca de threads redundantes.
— Noseratio 26/08/2015

10
OBSERVAÇÃO: As chamadas .Resultainda podem entrar em conflito se o chamador estiver no próprio pool de threads. Veja um cenário em que o Pool de Threads tem o tamanho 32 e 32 tarefas estão em execução e Wait()/Resultaguardando uma 33ª tarefa ainda a ser agendada que deseja executar em um dos threads em espera.
— Warty

55

Você deve obter o waititer ( GetAwaiter()) e encerrar a espera pela conclusão da tarefa assíncrona ( GetResult()).

string code = GenerateCodeAsync().GetAwaiter().GetResult();

38
Corremos em impasses usando esta solução. Esteja avisado.
— Oliver

6
MSDNTask.GetAwaiter : este método é destinado ao uso do compilador, e não ao código do aplicativo.
— foka

Ainda recebi o pop-up de diálogo de erro (contra minha vontade), com os botões 'Alternar para' ou 'Repetir'…. no entanto, a chamada é executada e retorna com uma resposta adequada.
— Jonathan Hansen

30

Você deve conseguir fazer isso usando delegados, expressão lambda

private void button2_Click(object sender, EventArgs e)
    {

        label1.Text = "waiting....";

        Task<string> sCode = Task.Run(async () =>
        {
            string msg =await GenerateCodeAsync();
            return msg;
        });

        label1.Text += sCode.Result;

    }

    private Task<string> GenerateCodeAsync()
    {
        return Task.Run<string>(() => GenerateCode());
    }

    private string GenerateCode()
    {
        Thread.Sleep(2000);
        return "I m back" ;
    }

Este trecho não será compilado. O tipo de retorno de Task.Run é Task. Veja este blog do MSDN para obter explicações completas.
— Appetere 27/08/15

5
Obrigado por apontar, sim, ele retorna o tipo de tarefa. Substituir "string sCode" por Task <string> ou var sCode deve resolver isso. Adicionando um código de compilação completo para facilitar.
— Faiyaz

20

Eu preciso chamar esse método de um método síncrono.

É possível com GenerateCodeAsync().Resultou GenerateCodeAsync().Wait(), como a outra resposta sugere. Isso bloquearia o encadeamento atual até a GenerateCodeAsyncconclusão.

No entanto, sua pergunta está marcada com e você também deixou o comentário:

Eu esperava uma solução mais simples, pensando que o asp.net lidava com isso muito mais facilmente do que escrever tantas linhas de código

Meu argumento é que você não deve estar bloqueando um método assíncrono no ASP.NET. Isso reduzirá a escalabilidade do seu aplicativo Web e poderá criar um impasse (quando uma awaitcontinuação interna GenerateCodeAsyncfor postada em AspNetSynchronizationContext). Usar Task.Run(...).Resultpara transferir algo para um encadeamento de pool e, em seguida, bloquear prejudicará ainda mais a escalabilidade, pois incorre em mais um encadeamento para processar uma determinada solicitação HTTP.

O ASP.NET possui suporte interno para métodos assíncronos, por meio de controladores assíncronos (no ASP.NET MVC e na Web API) ou diretamente via AsyncManagere PageAsyncTaskno ASP.NET clássico. Você deveria usá-lo. Para mais detalhes, verifique esta resposta .


Estou substituindo SaveChanges()método de DbContext, e aqui estou chamando os métodos assíncronos, por isso, infelizmente assíncrona controlador não vai me ajudar nesta situação
— Catalin

3
@RaraituL, em geral, você não mistura código assíncrono e sincronizado, escolhe outro modelo. Você pode implementar os dois SaveChangesAsynce SaveChangesapenas garantir que eles não sejam chamados de ambos no mesmo projeto do ASP.NET.
— noseratio

4
Nem todos os .NET MVCfiltros apoiar código assíncrono, por exemplo IAuthorizationFilter, por isso não posso usar asynctodo o caminho
— Catalin

3
@Noseratio que é uma meta irrealista. Existem muitas bibliotecas com código assíncrono e síncrono, além de situações nas quais o uso de apenas um modelo não é possível. Os MVC ActionFilters não suportam código assíncrono, por exemplo.
— 23615 Justin Skiles

9
@Noserato, a questão é chamar o método assíncrono do síncrono. Às vezes você não pode alterar a API que está implementando. Digamos que você implementou alguma interface síncrona de uma estrutura de terceiros "A" (não é possível reescrever a estrutura de maneira assíncrona), mas a biblioteca de terceiros "B" que você está tentando usar na sua implementação é apenas assíncrona. Também produto resultante também é biblioteca e pode ser usado em qualquer lugar, incluindo ASP.NET etc.
— dimzon

19

O Microsoft Identity possui métodos de extensão que chamam métodos assíncronos de forma síncrona. Por exemplo, existe o método GenerateUserIdentityAsync () e igual CreateIdentity ()

Se você olhar para UserManagerExtensions.CreateIdentity (), será assim:

 public static ClaimsIdentity CreateIdentity<TUser, TKey>(this UserManager<TUser, TKey> manager, TUser user,
        string authenticationType)
        where TKey : IEquatable<TKey>
        where TUser : class, IUser<TKey>
    {
        if (manager == null)
        {
            throw new ArgumentNullException("manager");
        }
        return AsyncHelper.RunSync(() => manager.CreateIdentityAsync(user, authenticationType));
    }

Agora vamos ver o que o AsyncHelper.RunSync faz

  public static TResult RunSync<TResult>(Func<Task<TResult>> func)
    {
        var cultureUi = CultureInfo.CurrentUICulture;
        var culture = CultureInfo.CurrentCulture;
        return _myTaskFactory.StartNew(() =>
        {
            Thread.CurrentThread.CurrentCulture = culture;
            Thread.CurrentThread.CurrentUICulture = cultureUi;
            return func();
        }).Unwrap().GetAwaiter().GetResult();
    }

Portanto, este é o seu invólucro para o método assíncrono. E por favor, não leia os dados do Result - ele potencialmente bloqueará seu código no ASP.

Existe outra maneira - que é suspeita para mim, mas você pode considerar isso também

  Result r = null;

            YourAsyncMethod()
                .ContinueWith(t =>
                {
                    r = t.Result;
                })
                .Wait();

3
Qual você considera o problema da segunda maneira sugerida?
— David Clarke

@DavidClarke provavelmente o problema de segurança do encadeamento de acessar uma variável não volátil a partir de vários encadeamentos sem bloqueio.
— Theodor Zoulias 06/07/19

9

Para evitar conflitos, sempre tento usar Task.Run()quando preciso chamar um método assíncrono de forma síncrona mencionada pelo @Heinzi.

No entanto, o método precisa ser modificado se o método assíncrono usar parâmetros. Por exemplo, Task.Run(GenerateCodeAsync("test")).Resultdá o erro:

Argumento 1: não é possível converter de ' System.Threading.Tasks.Task<string>' para 'System.Action'

Isso poderia ser chamado assim:

string code = Task.Run(() => GenerateCodeAsync("test")).Result;

6

A maioria das respostas neste segmento é complexa ou resultará em impasse.

O método a seguir é simples e evitará impasse, pois aguardamos a conclusão da tarefa e só então obtemos o resultado -

var task = Task.Run(() => GenerateCodeAsync()); 
task.Wait();
string code = task.Result;

Além disso, aqui está uma referência ao artigo do MSDN que fala exatamente da mesma coisa - https://blogs.msdn.microsoft.com/jpsanders/2017/08/28/asp-net-do-not-use-task-result- no contexto principal /


1

Eu prefiro uma abordagem sem bloqueio:

            Dim aw1=GenerateCodeAsync().GetAwaiter()
            While Not aw1.IsCompleted
                Application.DoEvents()
            End While

0

Bem, eu estou usando esta abordagem:

    private string RunSync()
    {
        var task = Task.Run(async () => await GenerateCodeService.GenerateCodeAsync());
        if (task.IsFaulted && task.Exception != null)
        {
            throw task.Exception;
        }

        return task.Result;
    }

-1

A outra maneira poderia ser se você quiser esperar até que a tarefa seja concluída:

var t = GenerateCodeService.GenerateCodeAsync();
Task.WhenAll(t);
string code = t.Result;

1
Isso está errado. WhenAll também retorna uma tarefa que você não está aguardando.
— Robert Schmidt

-1

EDITAR:

Task possui o método Wait, Task.Wait (), que aguarda a "promessa" resolver e, em seguida, continua, tornando-o síncrono. exemplo:


async Task<String> MyAsyncMethod() { ... }

String mySyncMethod() {

    return MyAsyncMethod().Wait();
}

3
Por favor, elabore sua resposta. Como isso é usado? Como especificamente ajuda a responder à pergunta?
— Scratte 29/04

-2

Se você tiver um método assíncrono chamado " RefreshList ", poderá chamar esse método assíncrono a partir de um método não assíncrono, como abaixo.

Task.Run(async () => { await RefreshList(); });
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.