Esperando de forma síncrona por uma operação assíncrona e por que o Wait () congela o programa aqui

318

Prefácio : estou procurando uma explicação, não apenas uma solução. Eu já conheço a solução.

Apesar de ter passado vários dias estudando artigos do MSDN sobre o padrão assíncrono baseado em tarefas (TAP), assíncrono e aguardando, ainda estou um pouco confuso sobre alguns dos detalhes.

Estou escrevendo um criador de logs para os aplicativos da Windows Store e quero oferecer suporte ao registro assíncrono e síncrono. Os métodos assíncronos seguem a TAP, os síncronos devem esconder tudo isso e parecer e funcionar como métodos comuns.

Este é o método principal de log assíncrono:

private async Task WriteToLogAsync(string text)
{
    StorageFolder folder = ApplicationData.Current.LocalFolder;
    StorageFile file = await folder.CreateFileAsync("log.log",
        CreationCollisionOption.OpenIfExists);
    await FileIO.AppendTextAsync(file, text,
        Windows.Storage.Streams.UnicodeEncoding.Utf8);
}

Agora, o método síncrono correspondente ...

Versão 1 :

private void WriteToLog(string text)
{
    Task task = WriteToLogAsync(text);
    task.Wait();
}

Parece correto, mas não funciona. Todo o programa congela para sempre.

Versão 2 :

Hmm .. Talvez a tarefa não tenha sido iniciada?

private void WriteToLog(string text)
{
    Task task = WriteToLogAsync(text);
    task.Start();
    task.Wait();
}

Isso joga InvalidOperationException: Start may not be called on a promise-style task.

Versão 3:

Hmm ... Task.RunSynchronouslyparece promissor.

private void WriteToLog(string text)
{
    Task task = WriteToLogAsync(text);
    task.RunSynchronously();
}

Isso joga InvalidOperationException: RunSynchronously may not be called on a task not bound to a delegate, such as the task returned from an asynchronous method.

Versão 4 (a solução):

private void WriteToLog(string text)
{
    var task = Task.Run(async () => { await WriteToLogAsync(text); });
    task.Wait();
}

Isso funciona. Então, 2 e 3 são as ferramentas erradas. Mas 1? O que há de errado com 1 e qual é a diferença para 4? O que faz 1 causar um congelamento? Há algum problema com o objeto de tarefa? Existe um impasse não óbvio?

Sebastian Negraszus
fonte
Alguma sorte em obter uma explicação em outro lugar? As respostas abaixo realmente não fornecem informações. Na verdade, eu estou usando .net 4.0 e não 4.5 / 5, então não posso usar algumas das operações, mas com os mesmos problemas.
amadib 29/03
3
@amadib, ver.1 e 4 foram explicados em [respostas fornecidas. Ver.2 e 3 tente iniciar novamente a tarefa já iniciada. Poste sua pergunta. Não está claro como você pode ter .NET 4.5 assíncronos questões / aguardam em .NET 4.0
Gennady Vanin Геннадий Ванин
1
A versão 4 é a melhor opção para o Xamarin Forms. Tentámos resto das opções e não funcionou e impasses experientes em todos os casos
Ramakrishna
Obrigado! A versão 4 funcionou para mim. Mas ainda é executado de forma assíncrona? Suponho que sim, porque a palavra-chave assíncrona está lá.
Sshirley

Respostas:

189

o await interior do seu método assíncrono está tentando voltar ao thread da interface do usuário.

Como o encadeamento da interface do usuário está ocupado aguardando a conclusão de toda a tarefa, você tem um impasse.

Mover a chamada assíncrona para Task.Run()resolver o problema.
Como a chamada assíncrona agora está sendo executada em um thread do conjunto de encadeamentos, ela não tenta retornar ao encadeamento da interface do usuário e, portanto, tudo funciona.

Como alternativa, você pode ligar StartAsTask().ConfigureAwait(false)antes de aguardar a operação interna para fazer com que ela volte ao pool de threads em vez do thread da interface do usuário, evitando completamente o conflito.

SLaks
fonte
9
+1. Aqui está mais uma explicação - Aguardar, interface do usuário e bloqueios! Oh meu!
Alexei Levenkov 23/01
13
A ConfigureAwait(false)é a solução apropriada neste caso. Como não é necessário chamar os retornos de chamada no contexto capturado, não deveria. Sendo um método de API, ele deve tratá-lo internamente, em vez de forçar todos os chamadores a sair do contexto da interface do usuário.
Servy
@ Service Estou perguntando desde que você mencionou o ConfigureAwait. Estou usando o .net3.5 e tive que remover o configure waitit porque não estava disponível na biblioteca assíncrona que estava usando. Como escrevo o meu próprio ou existe outra maneira de aguardar minha chamada assíncrona. Porque meu método também é interrompido. Eu não tenho tarefa, mas não Task.Run. Esse shoud provavelmente deve ser uma pergunta por si só.
flexxxit
@flexxxit: Você deve usar Microsoft.Bcl.Async.
SLaks
48

Chamar asynccódigo a partir de código síncrono pode ser bastante complicado.

Explico todos os motivos desse impasse no meu blog . Em resumo, há um "contexto" que é salvo por padrão no início de cada um awaite usado para retomar o método.

Portanto, se isso for chamado em um contexto de interface do usuário, quando a awaitconclusão for concluída, o asyncmétodo tentará entrar novamente nesse contexto para continuar executando. Infelizmente, o código que usa Wait(ou Result) bloqueia um segmento nesse contexto, portanto, oasync método não pode ser concluído.

As diretrizes para evitar isso são:

  1. Use ConfigureAwait(continueOnCapturedContext: false)o máximo possível. Isso permite que o seuasync métodos continuem executando sem precisar entrar novamente no contexto.
  2. Use asynctodo o caminho. Use em awaitvez de Resultou Wait.

Se seu método é naturalmente assíncrono, você (provavelmente) não deve expor um invólucro síncrono .

Stephen Cleary
fonte
Preciso executar uma tarefa assíncrona em um catch () que não suporta asynccomo eu faria isso e evito uma situação de incêndio e esquecimento.
Zapnologica 11/08/16
1
@Zapnologica: awaité suportado em catchblocos a partir do VS2015. Se você estiver em uma versão mais antiga, poderá atribuir a exceção a uma variável local e fazer o awaitdepois do bloco catch .
9788 Stephen Hawk
5

Aqui está o que eu fiz

private void myEvent_Handler(object sender, SomeEvent e)
{
  // I dont know how many times this event will fire
  Task t = new Task(() =>
  {
    if (something == true) 
    {
        DoSomething(e);  
    }
  });
  t.RunSynchronously();
}

funcionando muito bem e sem bloquear o thread da interface do usuário

pixel
fonte
0

Com um pequeno contexto de sincronização personalizado, a função de sincronização pode aguardar a conclusão da função assíncrona, sem criar conflito. Aqui está um pequeno exemplo para o aplicativo WinForms.

Imports System.Threading
Imports System.Runtime.CompilerServices

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        SyncMethod()
    End Sub

    ' waiting inside Sync method for finishing async method
    Public Sub SyncMethod()
        Dim sc As New SC
        sc.WaitForTask(AsyncMethod())
        sc.Release()
    End Sub

    Public Async Function AsyncMethod() As Task(Of Boolean)
        Await Task.Delay(1000)
        Return True
    End Function

End Class

Public Class SC
    Inherits SynchronizationContext

    Dim OldContext As SynchronizationContext
    Dim ContextThread As Thread

    Sub New()
        OldContext = SynchronizationContext.Current
        ContextThread = Thread.CurrentThread
        SynchronizationContext.SetSynchronizationContext(Me)
    End Sub

    Dim DataAcquired As New Object
    Dim WorkWaitingCount As Long = 0
    Dim ExtProc As SendOrPostCallback
    Dim ExtProcArg As Object

    <MethodImpl(MethodImplOptions.Synchronized)>
    Public Overrides Sub Post(d As SendOrPostCallback, state As Object)
        Interlocked.Increment(WorkWaitingCount)
        Monitor.Enter(DataAcquired)
        ExtProc = d
        ExtProcArg = state
        AwakeThread()
        Monitor.Wait(DataAcquired)
        Monitor.Exit(DataAcquired)
    End Sub

    Dim ThreadSleep As Long = 0

    Private Sub AwakeThread()
        If Interlocked.Read(ThreadSleep) > 0 Then ContextThread.Resume()
    End Sub

    Public Sub WaitForTask(Tsk As Task)
        Dim aw = Tsk.GetAwaiter

        If aw.IsCompleted Then Exit Sub

        While Interlocked.Read(WorkWaitingCount) > 0 Or aw.IsCompleted = False
            If Interlocked.Read(WorkWaitingCount) = 0 Then
                Interlocked.Increment(ThreadSleep)
                ContextThread.Suspend()
                Interlocked.Decrement(ThreadSleep)
            Else
                Interlocked.Decrement(WorkWaitingCount)
                Monitor.Enter(DataAcquired)
                Dim Proc = ExtProc
                Dim ProcArg = ExtProcArg
                Monitor.Pulse(DataAcquired)
                Monitor.Exit(DataAcquired)
                Proc(ProcArg)
            End If
        End While

    End Sub

     Public Sub Release()
         SynchronizationContext.SetSynchronizationContext(OldContext)
     End Sub

End Class
codefox
fonte