In attesa sincrono di un'operazione asincrona, e perché Wait () blocca qui il programma


318

Prefazione : sto cercando una spiegazione, non solo una soluzione. Conosco già la soluzione.

Nonostante abbia trascorso diversi giorni a studiare articoli MSDN sul Task-based Asynchronous Pattern (TAP), asincrono e attendo, sono ancora un po 'confuso su alcuni dei dettagli più fini.

Sto scrivendo un logger per le app di Windows Store e voglio supportare la registrazione sia asincrona che sincrona. I metodi asincroni seguono il TAP, quelli sincroni dovrebbero nascondere tutto ciò e apparire e funzionare come metodi ordinari.

Questo è il metodo principale di registrazione asincrona:

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);
}

Ora il corrispondente metodo sincrono ...

Versione 1 :

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

Sembra corretto, ma non funziona. L'intero programma si blocca per sempre.

Versione 2 :

Hmm .. Forse l'attività non è stata avviata?

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

Questo genera InvalidOperationException: Start may not be called on a promise-style task.

Versione 3:

Hmm .. Task.RunSynchronouslysembra promettente.

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

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

Versione 4 (la soluzione):

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

Questo funziona Quindi, 2 e 3 sono gli strumenti sbagliati. Ma 1? Cosa c'è di sbagliato in 1 e qual è la differenza con 4? Cosa fa 1 causare un congelamento? C'è qualche problema con l'oggetto task? C'è un deadlock non ovvio?


Hai fortuna a trovare una spiegazione altrove? Le risposte di seguito non forniscono informazioni. In realtà sto usando .net 4.0 non 4.5 / 5, quindi non posso usare alcune delle operazioni ma ho riscontrato gli stessi problemi.
amadib,

3
@amadib, ver.1 e 4 sono stati spiegati in [risposte fornite. Ver.2 e 3 provare a ricominciare attività già avviata. Pubblica la tua domanda. Non è chiaro come si possano avere problemi di 4.5 async /
wait di

1
La versione 4 è l'opzione migliore per Xamarin Forms. Abbiamo provato il resto delle opzioni e non abbiamo funzionato e abbiamo riscontrato deadlock in tutti i casi
Ramakrishna,

Grazie! La versione 4 ha funzionato per me. Ma funziona ancora in modo asincrono? Sto assumendo così perché la parola chiave asincrona è lì.
sshirley,

Risposte:


189

Il awaitall'interno del vostro metodo asincrono sta cercando di tornare al thread UI.

Poiché il thread dell'interfaccia utente è occupato in attesa del completamento dell'intera attività, è presente un deadlock.

Spostare la chiamata asincrona per Task.Run()risolvere il problema.
Poiché la chiamata asincrona è ora in esecuzione su un thread del pool di thread, non tenta di tornare al thread dell'interfaccia utente e pertanto tutto funziona.

In alternativa, è possibile chiamare StartAsTask().ConfigureAwait(false)prima di attendere l'operazione interna per tornare al pool di thread anziché al thread dell'interfaccia utente, evitando completamente il deadlock.


9
+1. Ecco un'altra spiegazione: attendere, UI e deadlock! Oh mio!
Alexei Levenkov,

13
Questa ConfigureAwait(false)è la soluzione appropriata in questo caso. Poiché non è necessario chiamare i callback nel contesto acquisito, non dovrebbe. Essendo un metodo API, dovrebbe gestirlo internamente, piuttosto che forzare tutti i chiamanti a uscire dal contesto dell'interfaccia utente.
Servito il

@Servy Sto chiedendo da quando hai citato ConfigureAwait. Sto usando .net3.5 e ho dovuto rimuovere configure attendi perché non era disponibile nella libreria asincrona che stavo usando. Come faccio a scrivere la mia o c'è un altro modo di attendere la mia chiamata asincrona. Perché anche il mio metodo si blocca. Non ho Task Ma non Task.Run. Questo shoud probabilmente è una domanda a sé stante.
flexxxit,

@flexxxit: dovresti usare Microsoft.Bcl.Async.
SLaks

48

Chiamare il asynccodice dal codice sincrono può essere piuttosto complicato.

Spiego le ragioni complete di questo deadlock sul mio blog . In breve, esiste un "contesto" che viene salvato per impostazione predefinita all'inizio di ciascuno awaite utilizzato per riprendere il metodo.

Quindi, se questo viene chiamato in un contesto UI, al awaittermine, il asyncmetodo tenta di accedere nuovamente a quel contesto per continuare l'esecuzione. Sfortunatamente, il codice usando Wait(o Result) bloccherà un thread in quel contesto, quindi il asyncmetodo non può essere completato.

Le linee guida per evitarlo sono:

  1. Usa ConfigureAwait(continueOnCapturedContext: false)il più possibile. Ciò consente asyncai metodi di continuare l'esecuzione senza dover rientrare nel contesto.
  2. Usa asyncfino in fondo. Utilizzare awaitinvece di Resulto Wait.

Se il tuo metodo è naturalmente asincrono, allora (probabilmente) non dovresti esporre un wrapper sincrono .


Devo eseguire un'attività Asincrona in un catch () che non supporta asynccome farei questo e prevenire un incendio e dimenticare la situazione.
Zapnologica,

1
@Zapnologica: awaitè supportato in catchblocchi a partire da VS2015. Se utilizzi una versione precedente, puoi assegnare l'eccezione a una variabile locale e procedere awaitdopo il blocco catch .
Stephen Cleary,

5

Ecco cosa ho fatto

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();
}

funziona alla grande e non blocca il thread dell'interfaccia utente


0

Con un piccolo contesto di sincronizzazione personalizzato, la funzione di sincronizzazione può attendere il completamento della funzione asincrona, senza creare deadlock. Ecco un piccolo esempio per l'app 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.