Synchroniczne oczekiwanie na operację asynchroniczną i dlaczego Wait () zamraża tutaj program

318

Przedmowa : Szukam wyjaśnienia, a nie tylko rozwiązania. Znam już rozwiązanie.

Mimo, że spędziłem kilka dni na studiowaniu artykułów MSDN na temat Asynchronicznego Wzorca Asynchronicznego (TAP), asynchronizacji i czekania, nadal jestem nieco zdezorientowany niektórymi szczegółami.

Piszę program rejestrujący dla aplikacji ze Sklepu Windows i chcę obsługiwać zarówno rejestrowanie asynchroniczne, jak i synchroniczne. Metody asynchroniczne są zgodne z TAP, metody synchroniczne powinny to wszystko ukrywać i wyglądać i działać jak zwykłe metody.

Jest to podstawowa metoda rejestrowania asynchronicznego:

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

Teraz odpowiednia metoda synchroniczna ...

Wersja 1 :

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

Wygląda to poprawnie, ale nie działa. Cały program zawiesza się na zawsze.

Wersja 2 :

Hmm .. Może zadanie nie zostało rozpoczęte?

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

To rzuca InvalidOperationException: Start may not be called on a promise-style task.

Wersja 3:

Hmm .. Task.RunSynchronouslybrzmi obiecująco.

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

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

Wersja 4 (rozwiązanie):

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

To działa. Zatem 2 i 3 to niewłaściwe narzędzia. Ale 1? Co jest nie tak z 1 i jaka jest różnica do 4? Co powoduje, że 1 powoduje zawieszenie się? Czy jest jakiś problem z obiektem zadania? Czy istnieje nieoczywisty impas?

Sebastian Negraszus
źródło
Jakieś szczęście, że gdzieś wyjaśnienie? Poniższe odpowiedzi naprawdę nie zapewniają wglądu. Właściwie używam .net 4.0 nie 4.5 / 5, więc nie mogę korzystać z niektórych operacji, ale mam te same problemy.
amadib
3
@amadib, wer. 1 i 4 zostały wyjaśnione w [odpowiedziach rpvided. Ver.2 i 3 spróbuj ponownie rozpocząć już uruchomione zadanie. Opublikuj swoje pytanie. Nie jest jasne, w jaki sposób można mieć asynchronię / oczekiwanie na problemy .NET 4.0 .NET 4.0
Gennady Vanin Геннадий Ванин
1
Wersja 4 jest najlepszą opcją dla Xamarin Forms. Wypróbowaliśmy resztę opcji, ale nie działaliśmy i we wszystkich przypadkach doświadczyliśmy impasu
Ramakrishna
Dzięki! Wersja 4 działała dla mnie. Ale czy nadal działa asynchronicznie? Zakładam, że tak, ponieważ istnieje słowo kluczowe asynchroniczne.
sshirley

Odpowiedzi:

189

awaitWewnątrz asynchronicznej metody próbuje wrócić do wątku UI.

Ponieważ wątek interfejsu użytkownika jest zajęty i czeka na zakończenie całego zadania, masz impas.

Przeniesienie wywołania asynchronicznego Task.Run()rozwiązuje problem.
Ponieważ wywołanie asynchroniczne działa teraz w wątku puli wątków, nie próbuje wrócić do wątku interfejsu użytkownika i dlatego wszystko działa.

Alternatywnie możesz zadzwonić StartAsTask().ConfigureAwait(false)przed oczekiwaniem na operację wewnętrzną, aby powrócić do puli wątków zamiast do wątku interfejsu użytkownika, całkowicie unikając impasu.

SLaks
źródło
9
+1. Oto jeszcze jedno wyjaśnienie - Oczekiwanie, interfejs użytkownika i impasy! O mój!
Aleksiej Lewenkow
13
W ConfigureAwait(false)tym przypadku jest właściwe rozwiązanie. Ponieważ nie ma potrzeby wywoływania wywołań zwrotnych w przechwyconym kontekście, nie powinno. Będąc metodą API, powinien obsługiwać ją wewnętrznie, zamiast zmuszać wszystkich wywołujących do opuszczenia kontekstu interfejsu użytkownika.
Servy
@Servy Pytam, odkąd wspomniałeś o ConfigureAwait. Korzystam z .net3.5 i musiałem usunąć konfigurację, czekam, ponieważ nie była dostępna w używanej bibliotece asynchronicznej. Jak napisać własne lub czy istnieje inny sposób oczekiwania na połączenie asynchroniczne. Bo moja metoda też się zawiesza. Nie mam Zadania, ale nie Zadania. Uruchom. To powinno być pytanie samo w sobie.
flexxxit
@flexxxit: Powinieneś użyć Microsoft.Bcl.Async.
SLaks,
48

Wywołanie asynckodu z kodu synchronicznego może być dość trudne.

Na moim blogu wyjaśniam pełne przyczyny tego impasu . Krótko mówiąc, istnieje „kontekst”, który jest domyślnie zapisywany na początku każdego awaiti wykorzystywany do wznowienia metody.

Jeśli więc zostanie to wywołane w kontekście interfejsu użytkownika, po awaitzakończeniu asyncmetoda podejmie próbę ponownego wprowadzenia tego kontekstu, aby kontynuować wykonywanie. Niestety kod używający Wait(lub Result) zablokuje wątek w tym kontekście, więc asyncmetoda nie może zostać zakończona.

Wytyczne, aby tego uniknąć, to:

  1. Używaj ConfigureAwait(continueOnCapturedContext: false)jak najwięcej. Umożliwia asyncto kontynuowanie wykonywania metod bez konieczności ponownego wchodzenia w kontekst.
  2. Użyj asyncdo końca. Użyj awaitzamiast Resultlub Wait.

Jeśli twoja metoda jest naturalnie asynchroniczna, to (prawdopodobnie) nie powinieneś wystawiać synchronicznego opakowania .

Stephen Cleary
źródło
Muszę wykonać zadanie asynchroniczne w catch (), która nie obsługuje asyncsposobu, w jaki to mam zrobić i zapobiec sytuacji pożaru i zapomnienia.
Zapnologica
1
@Zapnologica: awaitjest obsługiwany w catchblokach od VS2015. Jeśli korzystasz ze starszej wersji, możesz przypisać wyjątek do zmiennej lokalnej i wykonać awaitblok po catch .
Stephen Cleary,
5

Oto co zrobiłem

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

działa świetnie i nie blokuje wątku interfejsu użytkownika

piksel
źródło
0

Przy małym niestandardowym kontekście synchronizacji funkcja synchronizacji może czekać na zakończenie funkcji asynchronicznej bez tworzenia impasu. Oto mały przykład aplikacji 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
źródło