Como eu executaria um método de tarefa assíncrona <T> de forma síncrona?

628

Estou aprendendo sobre async / waitit e me deparei com uma situação em que preciso chamar um método assíncrono de forma síncrona. Como eu posso fazer isso?

Método assíncrono:

public async Task<Customers> GetCustomers()
{
    return await Service.GetCustomersAsync();
}

Uso normal:

public async void GetCustomers()
{
    customerList = await GetCustomers();
}

Eu tentei usar o seguinte:

Task<Customer> task = GetCustomers();
task.Wait()

Task<Customer> task = GetCustomers();
task.RunSynchronously();

Task<Customer> task = GetCustomers();
while(task.Status != TaskStatus.RanToCompletion)

Também tentei uma sugestão a partir daqui , no entanto, ela não funciona quando o expedidor está em um estado suspenso.

public static void WaitWithPumping(this Task task) 
{
        if (task == null) throw new ArgumentNullException(“task”);
        var nestedFrame = new DispatcherFrame();
        task.ContinueWith(_ => nestedFrame.Continue = false);
        Dispatcher.PushFrame(nestedFrame);
        task.Wait();
}

Aqui está a exceção e o rastreamento de pilha da chamada RunSynchronously:

System.InvalidOperationException

Mensagem : RunSynchronously não pode ser chamado em uma tarefa não acoplada a um delegado.

InnerException : null

Fonte : mscorlib

StackTrace :

          at System.Threading.Tasks.Task.InternalRunSynchronously(TaskScheduler scheduler)
   at System.Threading.Tasks.Task.RunSynchronously()
   at MyApplication.CustomControls.Controls.MyCustomControl.CreateAvailablePanelList() in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 638
   at MyApplication.CustomControls.Controls.MyCustomControl.get_AvailablePanels() in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 233
   at MyApplication.CustomControls.Controls.MyCustomControl.<CreateOpenPanelList>b__36(DesktopPanel panel) in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 597
   at System.Collections.Generic.List`1.ForEach(Action`1 action)
   at MyApplication.CustomControls.Controls.MyCustomControl.<CreateOpenPanelList>d__3b.MoveNext() in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 625
   at System.Runtime.CompilerServices.TaskAwaiter.<>c__DisplayClass7.<TrySetContinuationForAwait>b__1(Object state)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
   at System.Threading.ExecutionContext.runTryCode(Object userData)
   at System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.Run()
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run(Window window)
   at System.Windows.Application.Run()
   at MyApplication.App.Main() in C:\Documents and Settings\...\MyApplication\obj\Debug\App.g.cs:line 50
   at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
   at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
   at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()
Rachel
fonte
46
A melhor resposta para a pergunta "Como posso chamar um método assíncrono de forma síncrona" é "não". Existem hacks para tentar forçá-lo a funcionar, mas todos eles têm armadilhas muito sutis. Em vez disso, faça backup e corrija o código que faz com que você "precise" fazer isso.
Stephen Cleary
57
@ Stephen Cleary Concordo absolutamente, mas às vezes é simplesmente inevitável, como quando seu código depende de uma API de terceiros que não usa async / waitit. Além disso, se a ligação às propriedades do WPF ao usar o MVVM, é literalmente impossível usar async / waiting, pois isso não é suportado nas propriedades.
Contango
3
@StephenCleary Nem sempre. Estou construindo uma DLL que será importada no GeneXus . Ele não suporta palavras-chave assíncronas / em espera, portanto, devo usar apenas métodos síncronos.
Dinei
5
@StephenCleary 1) GeneXus é uma ferramenta de 3º pt e não tenho acesso ao seu código-fonte; 2) O GeneXus nem tem implementações de "funções", então não consigo perceber como implementar um "retorno de chamada" com esse tipo de coisa. Certamente seria uma solução mais difícil do que usar de forma Tasksíncrona; 3) Eu estou integrando GeneXus com MongoDB C motorista # , que expõem alguns métodos só de forma assíncrona
Dinei
1
@ygoe: Use um bloqueio compatível com assíncrono, como SemaphoreSlim.
Stephen Cleary

Respostas:

456

Aqui está uma solução que eu achei que funciona para todos os casos (incluindo expedidores suspensos). Não é meu código e ainda estou trabalhando para entendê-lo completamente, mas funciona.

Pode ser chamado usando:

customerList = AsyncHelpers.RunSync<List<Customer>>(() => GetCustomers());

O código é daqui

public static class AsyncHelpers
{
    /// <summary>
    /// Execute's an async Task<T> method which has a void return value synchronously
    /// </summary>
    /// <param name="task">Task<T> method to execute</param>
    public static void RunSync(Func<Task> task)
    {
        var oldContext = SynchronizationContext.Current;
        var synch = new ExclusiveSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(synch);
        synch.Post(async _ =>
        {
            try
            {
                await task();
            }
            catch (Exception e)
            {
                synch.InnerException = e;
                throw;
            }
            finally
            {
                synch.EndMessageLoop();
            }
        }, null);
        synch.BeginMessageLoop();

        SynchronizationContext.SetSynchronizationContext(oldContext);
    }

    /// <summary>
    /// Execute's an async Task<T> method which has a T return type synchronously
    /// </summary>
    /// <typeparam name="T">Return Type</typeparam>
    /// <param name="task">Task<T> method to execute</param>
    /// <returns></returns>
    public static T RunSync<T>(Func<Task<T>> task)
    {
        var oldContext = SynchronizationContext.Current;
        var synch = new ExclusiveSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(synch);
        T ret = default(T);
        synch.Post(async _ =>
        {
            try
            {
                ret = await task();
            }
            catch (Exception e)
            {
                synch.InnerException = e;
                throw;
            }
            finally
            {
                synch.EndMessageLoop();
            }
        }, null);
        synch.BeginMessageLoop();
        SynchronizationContext.SetSynchronizationContext(oldContext);
        return ret;
    }

    private class ExclusiveSynchronizationContext : SynchronizationContext
    {
        private bool done;
        public Exception InnerException { get; set; }
        readonly AutoResetEvent workItemsWaiting = new AutoResetEvent(false);
        readonly Queue<Tuple<SendOrPostCallback, object>> items =
            new Queue<Tuple<SendOrPostCallback, object>>();

        public override void Send(SendOrPostCallback d, object state)
        {
            throw new NotSupportedException("We cannot send to our same thread");
        }

        public override void Post(SendOrPostCallback d, object state)
        {
            lock (items)
            {
                items.Enqueue(Tuple.Create(d, state));
            }
            workItemsWaiting.Set();
        }

        public void EndMessageLoop()
        {
            Post(_ => done = true, null);
        }

        public void BeginMessageLoop()
        {
            while (!done)
            {
                Tuple<SendOrPostCallback, object> task = null;
                lock (items)
                {
                    if (items.Count > 0)
                    {
                        task = items.Dequeue();
                    }
                }
                if (task != null)
                {
                    task.Item1(task.Item2);
                    if (InnerException != null) // the method threw an exeption
                    {
                        throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException);
                    }
                }
                else
                {
                    workItemsWaiting.WaitOne();
                }
            }
        }

        public override SynchronizationContext CreateCopy()
        {
            return this;
        }
    }
}
Rachel
fonte
28
Para algumas informações sobre como isso funciona, Stephen Toub (Sr. Parallel) escreveu uma série de posts sobre isso. Parte 1 Parte 2 Parte 3
Cameron MacFarland
18
Atualizei o código de John para funcionar sem agrupar tarefas em lambdas: github.com/tejacques/AsyncBridge . Essencialmente, você trabalha com blocos assíncronos com a instrução using. Qualquer coisa dentro de um bloco usando acontece de forma assíncrona, com uma espera no final. A desvantagem é que você precisa desembrulhar a tarefa em um retorno de chamada, mas ainda é bastante elegante, especialmente se precisar chamar várias funções assíncronas ao mesmo tempo.
Tom Jacques
17
@StephenCleary Embora eu geralmente concorde com você que o código deve ser assíncrono até o fim, às vezes você se encontra em uma situação inviável em que é necessário forçá-lo como uma chamada síncrona. Basicamente, minha situação é que todo o meu código de acesso a dados é assíncrono. Eu precisava criar um mapa do site com base no mapa do site e a biblioteca de terceiros que eu estava usando era o MvcSitemap. Agora, quando alguém o está estendendo através da DynamicNodeProviderBaseclasse base, não é possível declará-lo como asyncmétodo. Eu tive que substituir por uma nova biblioteca ou simplesmente chamar uma operação síncrona.
Justin.lovell
6
@ justin.lovell: Sim, as limitações da biblioteca podem nos forçar a invadir, pelo menos até que a biblioteca seja atualizada. Parece que o MvcSitemap é uma situação em que é necessário um hack (filtros MVC e ações filho também); I pessoas simplesmente dissuadir deste em geral, porque hacks como este são maneira usado com muita freqüência quando estão não necessário. Com o MVC em particular, algumas APIs do ASP.NET/MVC assumem que possuem um AspNetSynchronizationContext, portanto esse hack específico não funcionará se você estiver chamando essas APIs.
Stephen Cleary
5
Este código não irá funcionar. Se for chamado a partir de um encadeamento de pool, ele poderá acionar um conflito de falta de encadeamento. O chamador bloqueará a espera pela conclusão da operação, o que pode nunca acontecer se ele tiver esgotado o pool de threads. Veja este artigo .
ZunTzu 27/10/2015
318

Esteja ciente de que esta resposta tem três anos. Eu o escrevi com base principalmente em uma experiência com .Net 4.0 e muito pouco com 4.5, especialmente com async-await. De um modo geral, é uma solução simples e agradável, mas às vezes quebra as coisas. Por favor, leia a discussão nos comentários.

.Net 4.5

Apenas use isto:

// For Task<T>: will block until the task is completed...
var result = task.Result; 

// For Task (not Task<T>): will block until the task is completed...
task2.RunSynchronously();

Consulte: TaskAwaiter , Task.Result , Task.RunSynchronously


.Net 4.0

Usa isto:

var x = (IAsyncResult)task;
task.Start();

x.AsyncWaitHandle.WaitOne();

...ou isto:

task.Start();
task.Wait();
AK_
fonte
67
.Resultpode produzir um impasse em certas do cenário
Jordy Langen
122
Resultpode facilmente causar um impasse no asynccódigo , como eu descrevo no meu blog.
Stephen Cleary
8
@StephenCleary Eu li o seu post e tentei eu mesmo. Eu honestamente acho que alguém na Microsoft estava muito bêbado ... É o mesmo problema que como winforms e fios fundo ....
AK_
9
A pergunta refere-se a uma tarefa retornada pelo método assíncrono. Esse tipo de tarefa já pode ter sido iniciado, executado ou cancelado; portanto, o uso do método Task.RunSynchronously pode resultar em InvalidOperationException . Consulte a página do MSDN: Task.RunSynchronously Method . Além disso, essa tarefa é provavelmente criado por Task.Factory.StartNew ou Task.Run métodos (método assíncrono para dentro), por isso é perigoso tentar iniciá-lo novamente. Algumas condições de corrida podem ocorrer em tempo de execução. Por outro lado, Task.Wait e Task.Result podem resultar em um impasse.
precisa saber é o seguinte
4
Executar Funcionou de forma síncrona para mim ... Não sei se estou faltando alguma coisa, mas isso parece preferível aos horrores da resposta marcada - eu estava apenas procurando uma maneira de desativar o modo assíncrono para testar o código que existe apenas para parar a interface do
usuário
121

Surpreso, ninguém mencionou isso:

public Task<int> BlahAsync()
{
    // ...
}

int result = BlahAsync().GetAwaiter().GetResult();

Não é tão bonito quanto alguns dos outros métodos aqui, mas possui os seguintes benefícios:

  • não engole exceções (como Wait)
  • não envolverá nenhuma exceção lançada em um AggregateException(como Result)
  • funciona para ambos Taske Task<T>( experimente você mesmo! )

Além disso, como GetAwaiteré tipado por pato, isso deve funcionar para qualquer objeto retornado de um método assíncrono (como ConfiguredAwaitableou YieldAwaitable), não apenas para Tarefas.


edit: Observe que é possível que essa abordagem (ou uso .Result) atinja um impasse, a menos que você adicione .ConfigureAwait(false)sempre que aguardar, todos os métodos assíncronos que podem ser alcançados BlahAsync()(não apenas aqueles que chama diretamente). Explicação .

// In BlahAsync() body
await FooAsync(); // BAD!
await FooAsync().ConfigureAwait(false); // Good... but make sure FooAsync() and
                                        // all its descendants use ConfigureAwait(false)
                                        // too. Then you can be sure that
                                        // BlahAsync().GetAwaiter().GetResult()
                                        // won't deadlock.

Se você tem preguiça de adicionar em .ConfigureAwait(false)qualquer lugar e não se importa com o desempenho, pode fazer alternativamente

Task.Run(() => BlahAsync()).GetAwaiter().GetResult()
James Ko
fonte
1
Funciona para mim para coisas simples. Além disso, se o método retornar um IAsyncOperation, eu teria que convertê-lo em uma tarefa primeiro: BlahAsync (). AsTask (). GetAwaiter (). GetResult ();
21716 Lee McPherson
3
Isso causou um conflito dentro de um método da web asmx. No entanto, envolvendo a chamada de método em um Task.Run () fez o trabalho:.. Task.Run (() => BlahAsync ()) GetAwaiter () GetResult ()
Augusto Barreto
Eu gosto mais dessa abordagem sintaticamente porque não envolve lambdas.
precisa saber é
25
NÃO edite as respostas de outras pessoas para inserir um link para você. Se você acha que sua resposta é melhor, deixe-a como um comentário.
Rachel
1
docs.microsoft.com/en-us/dotnet/api/… diz sobre GetAwaiter(): "Este método é destinado ao usuário do compilador, em vez de ser usado diretamente no código".
Theophilus
75

É muito mais simples executar a tarefa no pool de threads, em vez de tentar enganar o agendador para executá-la de forma síncrona. Dessa forma, você pode ter certeza de que não entrará em conflito. O desempenho é afetado devido à mudança de contexto.

Task<MyResult> DoSomethingAsync() { ... }

// Starts the asynchronous task on a thread-pool thread.
// Returns a proxy to the original task.
Task<MyResult> task = Task.Run(() => DoSomethingAsync());

// Will block until the task is completed...
MyResult result = task.Result; 
Michael L Perry
fonte
3
Então você chama task.Wait (). O tipo de dados é simplesmente Tarefa.
Michael L Perry
1
Vamos supor que DoSomethingAsync () seja o método assíncrono de longa duração como um todo (internamente, aguarda uma tarefa de longa duração), mas retorna rapidamente um controle de fluxo para o chamador, assim o trabalho do argumento lambda termina também rapidamente. O resultado de Tusk.Run () pode Task <Task> ou Task <Task <>> , portanto, você está aguardando um resultado de uma tarefa externa que é concluída rapidamente, mas uma tarefa interna (devido à espera de um trabalho de longa execução no método assíncrono) ainda está em execução. As conclusões são de que provavelmente precisamos usar a abordagem Unwrap () (como foi feita na publicação @ J.Lennon) para obter um comportamento síncrono do método assíncrono.
precisa saber é o seguinte
5
@sgnsajgon Você está errado. Task.Run é diferente de Task.Factory.StartNew, pois já desembrulha automaticamente o resultado. Veja este artigo .
ZunTzu
1
Posso apenas escrever Task.Run(DoSomethingAsync)? Isso remove um nível de delegados.
ygoe 13/12/19
1
Sim. Indo na direção oposta, porém, como em Task<MyResult> task = Task.Run(async () => await DoSomethingAsync());é mais explícito e aborda a preocupação do @sgnsajgon de que ele possa estar retornando uma tarefa <Task <MyResult>>. A sobrecarga correta do Task.Run é selecionada de qualquer maneira, mas o delegado assíncrono torna sua intenção óbvia.
Michael L Perry
57

Estou aprendendo sobre async / waitit e me deparei com uma situação em que preciso chamar um método assíncrono de forma síncrona. Como eu posso fazer isso?

A melhor resposta é você não , com os detalhes dependentes de qual é a "situação".

É um getter / setter de propriedade? Na maioria dos casos, é melhor ter métodos assíncronos do que "propriedades assíncronas". (Para mais informações, consulte minha postagem no blog sobre propriedades assíncronas ).

Este é um aplicativo MVVM e você deseja vincular dados assíncronos? Em seguida, use algo como o meu NotifyTask, conforme descrito no meu artigo do MSDN sobre ligação de dados assíncrona .

É um construtor? Então você provavelmente deseja considerar um método de fábrica assíncrono. (Para mais informações, consulte minha postagem no blog sobre construtores assíncronos ).

Quase sempre há uma resposta melhor do que fazer a sincronização sobre async.

Se não for possível para a sua situação (e você sabe disso fazendo uma pergunta aqui, descrevendo a situação ), recomendo apenas o uso de código síncrono. Async todo o caminho é o melhor; sincronizar todo o caminho é o segundo melhor. Sincronização por assíncrona não é recomendada.

No entanto, existem várias situações em que a sincronização sobre async é necessária. Especificamente, você está limitado pelo código de chamada de modo que você tem que ser sincronizado (e não têm absolutamente nenhuma maneira de re-pensar ou re-estrutura de seu código para permitir assincronia), e você tem que chamar assíncrono código. Essa é uma situação muito rara, mas surge de tempos em tempos.

Nesse caso, você precisaria usar um dos hacks descritos no meu artigo sobre desenvolvimento de brownfieldasync , especificamente:

  • Bloqueio (por exemplo, GetAwaiter().GetResult()). Observe que isso pode causar conflitos (como eu descrevo no meu blog).
  • Executando o código em um thread do pool de threads (por exemplo, Task.Run(..).GetAwaiter().GetResult()). Observe que isso só funcionará se o código assíncrono puder ser executado em um thread do pool de threads (ou seja, não depende de um contexto de interface do usuário ou ASP.NET).
  • Loops de mensagens aninhadas. Observe que isso só funcionará se o código assíncrono assumir apenas um contexto de thread único, não um tipo de contexto específico (muitos códigos de interface do usuário e ASP.NET esperam um contexto específico).

Os loops de mensagens aninhados são os mais perigosos de todos os hacks, porque causam reentrada . A reentrada é extremamente complicada de se pensar e (IMO) é a causa da maioria dos erros de aplicativos no Windows. Em particular, se você estiver no thread da interface do usuário e bloquear uma fila de trabalho (aguardando a conclusão do trabalho assíncrono), o CLR realmente enviará uma mensagem para você - ele realmente tratará algumas mensagens Win32 de dentro do seu código . Ah, e você não tem idéia de quais mensagens - quando Chris Brumme diz: "Não seria ótimo saber exatamente o que será bombeado? Infelizmente, bombear é uma arte negra que está além da compreensão mortal". , então realmente não temos esperança de saber.

Portanto, quando você bloqueia assim em um thread da interface do usuário, está pedindo problemas. Outra cbrumme cita o mesmo artigo: "De tempos em tempos, clientes dentro ou fora da empresa descobrem que estamos enviando mensagens durante o bloqueio gerenciado em um STA [thread da interface do usuário]. Essa é uma preocupação legítima, porque eles sabem que é muito difícil escrever código robusto diante da reentrada ".

Sim, ele é. É muito difícil escrever código robusto diante da reentrada. E os loops de mensagens aninhados forçam você a escrever um código robusto diante da reentrada. É por isso que a resposta aceita (e mais votada) para essa pergunta é extremamente perigosa na prática.

Se você estiver completamente sem todas as outras opções - não poderá reprojetar seu código, não poderá reestruturá-lo para ser assíncrono - será forçado pelo código de chamada imutável a ser sincronizado - não poderá alterar o código downstream para sincronização - você não pode bloquear - você não pode executar o código assíncrono em um thread separado - então e somente então você deve considerar adotar a reentrada.

Se você se encontrar nesse canto, recomendo usar algo como Dispatcher.PushFramepara aplicativos WPF , alternar com Application.DoEventsaplicativos WinForm e, no caso geral, o meu AsyncContext.Run.

Stephen Cleary
fonte
Stephen, há outra pergunta muito semelhante à qual você também forneceu uma resposta impressionante. Você acha que um deles pode ser fechado como duplicado ou mesclar solicitação ou exibir meta primeiro (já que cada q tem ~ 200K views + de 200 votos)? Sugestões?
Alexei Levenkov 5/02
1
@AlexeiLevenkov: Não me sinto bem ao fazer isso, por algumas razões: 1) A resposta à pergunta vinculada está bastante desatualizada. 2) Escrevi um artigo inteiro sobre o assunto que considero mais completo do que qualquer SO Q / A existente. 3) A resposta aceita nesta pergunta é extremamente popular. 4) Sou veementemente contra essa resposta aceita. Então, encerrar isso como uma mentira seria um abuso de poder; concluir que, como uma bobagem disso (ou fusão), daria uma resposta perigosa ainda mais. Deixei e deixei para a comunidade.
Stephen Cleary
Está bem. Vou considerar abordá-lo na meta do que de alguma forma.
Alexei Levenkov 06/02
9
Essa resposta vai muito além da minha cabeça. "Usar async até o fim" é um conselho confuso, porque claramente não é possível segui-lo. Um programa com um Main()método assíncrono não compila; em algum momento, você precisa preencher a lacuna entre os mundos sincronizado e assíncrono. Não é uma " situação muito rara" , é necessária literalmente em todos os programas que chamam um método assíncrono. Não há opção para não "sincronizar sobre async" , apenas uma opção para desviar essa carga do método de chamada, em vez de carregá-lo no que você está escrevendo no momento.
Mark Amery
1
Ótimo. Estou prestes a aplicar asynctodos os métodos no meu aplicativo agora. E isso é muito. Isso não pode ser apenas o padrão?
ygoe 13/12/19
25

Se estou lendo sua pergunta corretamente - o código que deseja a chamada síncrona para um método assíncrono está sendo executado em um encadeamento de expedidor suspenso. E você deseja realmente bloquear esse segmento de forma síncrona até que o método assíncrono seja concluído.

Os métodos assíncronos no C # 5 são alimentados cortando efetivamente o método em pedaços sob o capô e retornando um Taskque pode rastrear a conclusão geral de todo o shabang. No entanto, a maneira como os métodos cortados são executados pode depender do tipo de expressão transmitida ao awaitoperador.

Na maioria das vezes, você estará usando awaituma expressão do tipo Task. A implementação do awaitpadrão da tarefa é "inteligente", pois adia para o SynchronizationContext, o que basicamente causa o seguinte:

  1. Se o encadeamento que entra no awaitestá em um encadeamento de loop de mensagem Dispatcher ou WinForms, ele garante que os pedaços do método assíncrono ocorram como parte do processamento da fila de mensagens.
  2. Se o encadeamento que entra no awaitestá em um encadeamento de conjuntos de encadeamentos, os pedaços restantes do método assíncrono ocorrem em qualquer lugar no conjunto de encadeamentos.

É por isso que você provavelmente está tendo problemas - a implementação do método assíncrono está tentando executar o restante no Dispatcher - mesmo que esteja suspenso.

.... fazendo backup! ....

Eu tenho que fazer a pergunta, por que você está tentando bloquear de forma síncrona um método assíncrono? Fazer isso anularia o propósito de por que o método queria ser chamado de forma assíncrona. Em geral, quando você começa a usar awaitum método Dispatcher ou UI, deseja ativar todo o fluxo da UI de forma assíncrona. Por exemplo, se sua pilha de chamadas era algo como o seguinte:

  1. [Topo] WebRequest.GetResponse()
  2. YourCode.HelperMethod()
  3. YourCode.AnotherMethod()
  4. YourCode.EventHandlerMethod()
  5. [UI Code].Plumbing()- WPFou WinFormscódigo
  6. [Loop de mensagem] - WPFou WinFormsLoop de mensagem

Depois que o código for transformado para usar assíncrono, você normalmente terminará com

  1. [Topo] WebRequest.GetResponseAsync()
  2. YourCode.HelperMethodAsync()
  3. YourCode.AnotherMethodAsync()
  4. YourCode.EventHandlerMethodAsync()
  5. [UI Code].Plumbing()- WPFou WinFormscódigo
  6. [Loop de mensagem] - WPFou WinFormsLoop de mensagem

Atendendo realmente

A classe AsyncHelpers acima realmente funciona porque se comporta como um loop de mensagens aninhadas, mas instala sua própria mecânica paralela no Dispatcher em vez de tentar executar no próprio Dispatcher. Essa é uma solução alternativa para o seu problema.

Outra solução alternativa é executar o método assíncrono em um thread do pool de threads e aguarde a conclusão. Fazer isso é fácil - você pode fazer isso com o seguinte snippet:

var customerList = TaskEx.RunEx(GetCustomers).Result;

A API final será Task.Run (...), mas com o CTP você precisará dos sufixos Ex ( explicação aqui ).

Theo Yaung
fonte
+1 para a explicação detalhada, no entanto, TaskEx.RunEx(GetCustomers).Resulttrava o aplicativo quando ele é executado em um encadeamento de expedidor suspenso. Além disso, o método GetCustomers () normalmente é executado de forma assíncrona; no entanto, em uma situação, ele precisa ser executado de forma síncrona, então eu estava procurando uma maneira de fazer isso sem criar uma versão de sincronização do método.
24511 Rachel
+1 em "por que você está tentando bloquear de forma síncrona em um método assíncrono?" Sempre existe uma maneira de usar asyncmétodos adequadamente ; loops aninhados certamente devem ser evitados.
Stephen Cleary
24

Isso está funcionando bem para mim

public static class TaskHelper
{
    public static void RunTaskSynchronously(this Task t)
    {
        var task = Task.Run(async () => await t);
        task.Wait();
    }

    public static T RunTaskSynchronously<T>(this Task<T> t)
    {
        T res = default(T);
        var task = Task.Run(async () => res = await t);
        task.Wait();
        return res;
    }
}
Clemente
fonte
Você também deve usar o método Task.Unwrap , porque sua instrução Task.Wait causa a espera pela tarefa externa (criada por Task.Run ), e não pela espera interna t Tarefa passada como parâmetro do método de extensão. Seu método Task.Run não retorna a tarefa <T>, mas a tarefa <Task <T>>. Em alguns cenários simples, sua solução pode funcionar devido às otimizações do TaskScheduler, por exemplo, usando o método TryExecuteTaskInline para executar tarefas no thread atual durante a operação Wait . Observe o meu comentário nesta resposta.
precisa saber é o seguinte
1
Isso não está correto. O Task.Run retornará a tarefa <T>. Veja essa sobrecarga msdn.microsoft.com/en-us/library/hh194918(v=vs.110).aspx
Clement
Como isso deve ser usado? Este deadlocks no WPF:MyAsyncMethod().RunTaskSynchronously();
ygoe 26/09/16
18

A maneira mais simples de executar tarefas de forma síncrona e sem bloquear o thread da interface do usuário é usar RunSynchronously () como:

Task t = new Task(() => 
{ 
   //.... YOUR CODE ....
});
t.RunSynchronously();

No meu caso, eu tenho um evento que é acionado quando algo ocorre. Não sei quantas vezes isso ocorrerá. Então, eu uso o código acima no meu evento; portanto, sempre que é acionado, ele cria uma tarefa. As tarefas são executadas de forma síncrona e funcionam muito bem para mim. Fiquei surpreso que demorei tanto para descobrir isso, considerando como é simples. Geralmente, as recomendações são muito mais complexas e propensas a erros. Foi assim que é simples e limpo.

pixel
fonte
1
Mas como podemos usar esse método quando o código assíncrono retorna algo que precisamos?
S.Serpooshan
16

Já o enfrentei algumas vezes, principalmente em testes de unidade ou no desenvolvimento de serviços do Windows. Atualmente, eu sempre uso esse recurso:

        var runSync = Task.Factory.StartNew(new Func<Task>(async () =>
        {
            Trace.WriteLine("Task runSync Start");
            await TaskEx.Delay(2000); // Simulates a method that returns a task and
                                      // inside it is possible that there
                                      // async keywords or anothers tasks
            Trace.WriteLine("Task runSync Completed");
        })).Unwrap();
        Trace.WriteLine("Before runSync Wait");
        runSync.Wait();
        Trace.WriteLine("After runSync Waited");

É simples, fácil e não tive problemas.

J. Lennon
fonte
Este é o único que não entrou em conflito para mim.
AndreFeijo 16/02
15

Encontrei esse código no componente Microsoft.AspNet.Identity.Core e funciona.

private static readonly TaskFactory _myTaskFactory = new 
     TaskFactory(CancellationToken.None, TaskCreationOptions.None, 
     TaskContinuationOptions.None, TaskScheduler.Default);

// Microsoft.AspNet.Identity.AsyncHelper
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
    CultureInfo cultureUi = CultureInfo.CurrentUICulture;
    CultureInfo culture = CultureInfo.CurrentCulture;
    return AsyncHelper._myTaskFactory.StartNew<Task<TResult>>(delegate
    {
        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = cultureUi;
        return func();
    }).Unwrap<TResult>().GetAwaiter().GetResult();
}
wenhx
fonte
13

Apenas uma pequena nota - esta abordagem:

Task<Customer> task = GetCustomers();
task.Wait()

funciona para o WinRT.

Deixe-me explicar:

private void TestMethod()
{
    Task<Customer> task = GetCustomers(); // call async method as sync and get task as result
    task.Wait(); // wait executing the method
    var customer = task.Result; // get's result.
    Debug.WriteLine(customer.Name); //print customer name
}
public class Customer
{
    public Customer()
    {
        new ManualResetEvent(false).WaitOne(TimeSpan.FromSeconds(5));//wait 5 second (long term operation)
    }
    public string Name { get; set; }
}
private Task<Customer> GetCustomers()
{
    return Task.Run(() => new Customer
    {
        Name = "MyName"
    });
}

Além disso, essa abordagem funciona apenas para soluções da Windows Store!

Nota: Dessa forma, o thread não é seguro se você chamar seu método dentro de outro método assíncrono (de acordo com os comentários do @Servy)

RredCat
fonte
Expliquei esta solução, consulte a seção EDIT.
RredCat
2
Isso pode facilmente resultar em conflitos quando chamado em situações assíncronas.
Servy
@Servy faz sentido. Então, quando eu estiver correto, usar Wait (timeOut) pode ajudar, certo?
RredCat
1
Então, você precisa se preocupar em ter atingido o tempo limite quando a operação não for realmente concluída, o que é muito ruim e também o tempo gasto até o tempo limite nos casos em que ocorre o bloqueio (e, nesse caso, você ainda continua quando não estiver pronto). Então não, isso não resolve o problema.
Servy
@ Service Parece que eu tenho que implementar CancellationTokena minha solução.
RredCat
10

No seu código, sua primeira espera pela execução da tarefa, mas você não a iniciou, pelo que aguarda indefinidamente. Tente o seguinte:

Task<Customer> task = GetCustomers();
task.RunSynchronously();

Editar:

Você diz que recebe uma exceção. Poste mais detalhes, incluindo rastreamento de pilha.
Mono contém o seguinte caso de teste:

[Test]
public void ExecuteSynchronouslyTest ()
{
        var val = 0;
        Task t = new Task (() => { Thread.Sleep (100); val = 1; });
        t.RunSynchronously ();

        Assert.AreEqual (1, val);
}

Verifique se isso funciona para você. Se isso não acontecer, apesar de muito improvável, você pode ter uma compilação estranha do CTP assíncrono. Se funcionar, convém examinar o que exatamente o compilador gera e como a Taskinstanciação é diferente desse exemplo.

Edição # 2:

Eu verifiquei com o Reflector que a exceção que você descreveu ocorre quando m_actioné null. Isso é meio estranho, mas não sou especialista em CTP assíncrono. Como eu disse, você deve descompilar seu código e ver exatamente como Taskestá sendo instanciado qualquer como é que os seus m_actioné null.


PS Qual é o problema dos votos ocasionais? Cuidado ao elaborar?

Dan Abramov
fonte
Ajustei minha pergunta para tornar o código que eu havia tentado um pouco mais claro. RunSynchronously retorna um erro de RunSynchronously may not be called on a task unbound to a delegate. Google é nenhuma ajuda desde que todos os resultados para que estão em chinês ...
Rachel
Penso que a diferença é que não crio a tarefa e, em seguida, tento executá-la. Em vez disso, a tarefa é criada pelo método assíncrono quando a awaitpalavra-chave é usada. A exceção postada em meu comentário anterior é a exceção que recebo, apesar de ser uma das poucas para as quais não posso pesquisar no Google e encontrar uma causa ou solução.
23411 Rachel
1
asynce as asyncpalavras - chave não passam de açúcar de sintaxe. Compilador gera código para criar Task<Customer>, GetCustomers()então é aí que eu procuraria primeiro. Quanto à exceção, você postou apenas a mensagem de exceção, que é inútil sem o tipo de exceção e o rastreamento de pilha. Chame o ToString()método da exceção e publique a saída na pergunta.
Dan Abramov 23/02
@ gaearon: eu postei os detalhes da exceção e o rastreamento da pilha na minha pergunta original.
Rachel
2
@ gaearon Acho que você recebeu votos negativos porque sua postagem não é aplicável a perguntas. A discussão é sobre métodos de espera assíncrona, não sobre métodos simples de retorno de tarefas. Além disso, na minha opinião, o mecanismo de espera assíncrona é um açúcar sintático, mas não tão trivial - há continuação, captura de contexto, retomada de contexto local, manipulação aprimorada de exceções locais e muito mais. Em seguida, você não deve chamar o método RunSynchronously no resultado do método assíncrono, porque, por definição, o método assíncrono deve retornar a tarefa que está atualmente pelo menos agendada e mais de uma vez está no estado de execução.
precisa saber é o seguinte
9

Testado em .Net 4.6. Também pode evitar um conflito.

Para método assíncrono retornando Task.

Task DoSomeWork();
Task.Run(async () => await DoSomeWork()).Wait();

Para método assíncrono retornando Task<T>

Task<T> GetSomeValue();
var result = Task.Run(() => GetSomeValue()).Result;

Editar :

Se o chamador estiver em execução no thread do conjunto de encadeamentos (ou o chamador também estiver em uma tarefa), ainda poderá causar um conflito em alguma situação.

Liang
fonte
1
Minha resposta após quase 8 anos :) O segundo exemplo - produzirá um impasse em todo o contexto agendado usado principalmente (aplicativo de console / núcleo .NET / aplicativo de desktop / ...). aqui você tem mais visão geral do que estou falando agora: medium.com/rubrikkgroup/…
W92 14/02/19
Resulté perfeito para o trabalho se você deseja uma chamada síncrona e, de outra forma, perigoso. Não há nada no nome Resultou no sentido intelectual Resultque indique que se trata de uma chamada bloqueada. Realmente deve ser renomeado.
Zodman 24/04
5

use o código abaixo

Task.WaitAll(Task.Run(async () => await service.myAsyncMethod()));
Mahesh
fonte
4

Por que não criar uma chamada como:

Service.GetCustomers();

isso não é assíncrono.

Daniel A. White
fonte
4
Isso vai ser o que eu faço se eu não posso começar este trabalho ... criar uma versão de sincronização, além de uma versão assíncrona
Rachel
3

Esta resposta foi projetada para quem está usando o WPF for .NET 4.5.

Se você tentar executar Task.Run()no encadeamento da GUI, task.Wait()ficará travado indefinidamente, se você não tiver oasync palavra chave em sua definição de função.

Esse método de extensão resolve o problema verificando se estamos no encadeamento da GUI e, se estiver, executando a tarefa no encadeamento do despachante WPF.

Essa classe pode atuar como a cola entre o mundo assíncrono / aguardar e o mundo não assíncrono / aguardar, em situações onde é inevitável, como propriedades do MVVM ou dependências de outras APIs que não usam async / wait.

/// <summary>
///     Intent: runs an async/await task synchronously. Designed for use with WPF.
///     Normally, under WPF, if task.Wait() is executed on the GUI thread without async
///     in the function signature, it will hang with a threading deadlock, this class 
///     solves that problem.
/// </summary>
public static class TaskHelper
{
    public static void MyRunTaskSynchronously(this Task task)
    {
        if (MyIfWpfDispatcherThread)
        {
            var result = Dispatcher.CurrentDispatcher.InvokeAsync(async () => { await task; });
            result.Wait();
            if (result.Status != DispatcherOperationStatus.Completed)
            {
                throw new Exception("Error E99213. Task did not run to completion.");
            }
        }
        else
        {
            task.Wait();
            if (task.Status != TaskStatus.RanToCompletion)
            {
                throw new Exception("Error E33213. Task did not run to completion.");
            }
        }
    }

    public static T MyRunTaskSynchronously<T>(this Task<T> task)
    {       
        if (MyIfWpfDispatcherThread)
        {
            T res = default(T);
            var result = Dispatcher.CurrentDispatcher.InvokeAsync(async () => { res = await task; });
            result.Wait();
            if (result.Status != DispatcherOperationStatus.Completed)
            {
                throw new Exception("Error E89213. Task did not run to completion.");
            }
            return res;
        }
        else
        {
            T res = default(T);
            var result = Task.Run(async () => res = await task);
            result.Wait();
            if (result.Status != TaskStatus.RanToCompletion)
            {
                throw new Exception("Error E12823. Task did not run to completion.");
            }
            return res;
        }
    }

    /// <summary>
    ///     If the task is running on the WPF dispatcher thread.
    /// </summary>
    public static bool MyIfWpfDispatcherThread
    {
        get
        {
            return Application.Current.Dispatcher.CheckAccess();
        }
    }
}
Contango
fonte
3

Simplesmente ligar .Result;ou .Wait()é um risco de impasse, como muitos disseram nos comentários. Como a maioria de nós gosta de oneliners, você pode usá-los para.Net 4.5<

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

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

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

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

Nenhum problema de deadlock ocorrerá devido ao uso de Task.Run .

Fonte:

https://stackoverflow.com/a/32429753/3850405

Ogglas
fonte
1

Penso que o seguinte método auxiliar também pode resolver o problema.

private TResult InvokeAsyncFuncSynchronously<TResult>(Func< Task<TResult>> func)
    {
        TResult result = default(TResult);
        var autoResetEvent = new AutoResetEvent(false);

        Task.Run(async () =>
        {
            try
            {
                result = await func();
            }
            catch (Exception exc)
            {
                mErrorLogger.LogError(exc.ToString());
            }
            finally
            {
                autoResetEvent.Set();
            }
        });
        autoResetEvent.WaitOne();

        return result;
    }

Pode ser usado da seguinte maneira:

InvokeAsyncFuncSynchronously(Service.GetCustomersAsync);
donttellya
fonte
1
Por favor, explique a votação
donttellya
2
... Ainda estou ansiosamente interessado por que esta resposta foi rejeitada?
precisa saber é o seguinte
Não é um "síncrono" verdadeiro. Você cria dois threads e espera nos primeiros resultados de outro.
TMT
e todas as coisas de lado, essa é uma péssima idéia.
Dan Pantry
1
Acabei de escrever quase o código idêntico (linha por linha igual), mas usando o SemaphoreSlim em vez do evento de redefinição automática. Gostaria de ter visto isso antes. Eu acho essa abordagem para evitar conflitos e mantém o código assíncrono em execução da mesma maneira que em cenários assíncronos verdadeiros. Não sei ao certo por que isso é uma má ideia. Parece muito mais limpo do que as outras abordagens que eu já vi acima.
tmrog
0

Isso funciona para mim

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    public static class AsyncHelper
    {
        private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);

        public static void RunSync(Func<Task> func)
        {
            _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
        }

        public static TResult RunSync<TResult>(Func<Task<TResult>> func)
        {
            return _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
        }
    }

    class SomeClass
    {
        public async Task<object> LoginAsync(object loginInfo)
        {
            return await Task.FromResult(0);
        }
        public object Login(object loginInfo)
        {
            return AsyncHelper.RunSync(() => LoginAsync(loginInfo));
            //return this.LoginAsync(loginInfo).Result.Content;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var someClass = new SomeClass();

            Console.WriteLine(someClass.Login(1));
            Console.ReadLine();
        }
    }
}
Dan Nguyen
fonte
-1

Eu descobri que o SpinWait funciona muito bem para isso.

var task = Task.Run(()=>DoSomethingAsyncronous());

if(!SpinWait.SpinUntil(()=>task.IsComplete, TimeSpan.FromSeconds(30)))
{//Task didn't complete within 30 seconds, fail...
   return false;
}

return true;

A abordagem acima não precisa usar .Result ou .Wait (). Ele também permite especificar um tempo limite para que você não fique preso para sempre, caso a tarefa nunca seja concluída.

Curtis
fonte
1
O voto negativo sugere que alguém não gosta desse método. Existe alguém que possa comentar o lado negativo disso?
Grax32 #
Na ausência do voto negativo dizendo POR QUE o voto negativo foi dado, alguém pode votá-lo? :-)
Curtis
1
Isso é polling (rotação), o delegado receberá o thread do pool até 1000 vezes por segundo. Pode não retornar o controle imediatamente após a conclusão da tarefa ( erro de até 10 + ms ). Se concluída pelo tempo limite, a tarefa continuará em execução, o que torna o tempo limite praticamente inútil.
Sinatr 20/05/19
Na verdade, eu estou usando isso em todo o lugar no meu código e quando a condição é atendida, o SpinWaitSpinUntil () sai imediatamente. Portanto, o que ocorrer primeiro, 'condição atendida' ou tempo limite, a tarefa será encerrada. Ele não continua sendo executado.
Curtis
-3

No wp8:

Embrulhe-o:

Task GetCustomersSynchronously()
{
    Task t = new Task(async () =>
    {
        myCustomers = await GetCustomers();
    }
    t.RunSynchronously();
}

Chame-o:

GetCustomersSynchronously();
user2113284
fonte
3
Não, isso não vai funcionar, porque a tarefa não esperam o delegado a partir do construtor (seu delegado um e não uma tarefa ..)
Rico Suter
-4
    private int GetSync()
    {
        try
        {
            ManualResetEvent mre = new ManualResetEvent(false);
            int result = null;

            Parallel.Invoke(async () =>
            {
                result = await SomeCalcAsync(5+5);
                mre.Set();
            });

            mre.WaitOne();
            return result;
        }
        catch (Exception)
        {
            return null;
        }
    }
ksemenenko
fonte
-5

Ou você pode apenas ir com:

customerList = Task.Run<List<Customer>>(() => { return GetCustomers(); }).Result;

Para isso compilar, certifique-se de referenciar o conjunto de extensão:

System.Net.Http.Formatting
user2057962
fonte
-9

Tente o seguinte código que funciona para mim:

public async void TaskSearchOnTaskList (SearchModel searchModel)
{
    try
    {
        List<EventsTasksModel> taskSearchList = await Task.Run(
            () => MakeasyncSearchRequest(searchModel),
            cancelTaskSearchToken.Token);

        if (cancelTaskSearchToken.IsCancellationRequested
                || string.IsNullOrEmpty(rid_agendaview_search_eventsbox.Text))
        {
            return;
        }

        if (taskSearchList == null || taskSearchList[0].result == Constants.ZERO)
        {
            RunOnUiThread(() => {
                textViewNoMembers.Visibility = ViewStates.Visible;                  
                taskListView.Visibility = ViewStates.Gone;
            });

            taskSearchRecureList = null;

            return;
        }
        else
        {
            taskSearchRecureList = TaskFooterServiceLayer
                                       .GetRecurringEvent(taskSearchList);

            this.SetOnAdapter(taskSearchRecureList);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("ActivityTaskFooter -> TaskSearchOnTaskList:" + ex.Message);
    }
}
gandhraj gayakwad
fonte