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;
}
Catalin
fonte
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:

Heinzi
fonte
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();
Diego Torres
fonte
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" ;
    }
Faiyaz
fonte
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 .

noseratio
fonte
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();
Vitaliy Markitanov
fonte
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;
Ogglas
fonte
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 /

kamalpreet
fonte
1

Eu prefiro uma abordagem sem bloqueio:

            Dim aw1=GenerateCodeAsync().GetAwaiter()
            While Not aw1.IsCompleted
                Application.DoEvents()
            End While
Zibri
fonte
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;
    }
Jiří Herník
fonte
-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;
frablaser
fonte
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();
}
Avi Tshuva
fonte
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(); });
dush88c
fonte