web-dev-qa-db-ja.com

非同期操作を同期的に待機し、そしてここでWait()がプログラムをフリーズするのはなぜですか

序文:私は解決策ではなく説明を探しています。私はすでに解決策を知っています。

タスクベースの非同期パターン(TAP)、非同期、待機についてのMSDNの記事の調査に数日を費やしたにもかかわらず、私はまだ細かい点について少し混乱しています。

私はWindowsストアアプリ用のロガーを書いています、そして私は非同期と同期の両方のロギングをサポートしたいです。非同期メソッドはTAPに従い、同期メソッドはこれをすべて隠し、通常のメソッドと同じように見えて動作するはずです。

これが非同期ロギングの中心的な方法です。

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

今対応する同期方法...

バージョン1

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

これは正しいように見えますが、機能しません。プログラム全体が永久にフリーズします。

バージョン2

うーん..おそらくタスクが開始されていないのですか?

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

これはInvalidOperationException: Start may not be called on a promise-style task.を投げます

バージョン3:

うーん.. Task.RunSynchronouslyは有望に聞こえます。

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

これはInvalidOperationException: RunSynchronously may not be called on a task not bound to a delegate, such as the task returned from an asynchronous method.を投げます

バージョン4(解決策):

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

これはうまくいきます。したがって、2と3は間違ったツールです。しかし1? 1の何が問題になっていますか?4との違いは何ですか? 1がフリーズを引き起こす原因は何ですか?タスクオブジェクトに問題はありますか?明白でないデッドロックはありますか?

283

非同期メソッド内のawaitは、UIスレッドに戻ろうとしています。

UIスレッドはタスク全体の完了を待って忙しいので、デッドロックがあります。

非同期呼び出しをTask.Run()に移動すると問題は解決します。
非同期呼び出しは現在スレッドプールスレッドで実行されているため、UIスレッドに戻ってくることはなく、したがってすべてが機能します。

あるいは、内部操作を待つ前にStartAsTask().ConfigureAwait(false)を呼び出して、UIスレッドではなくスレッドプールに戻すことで、デッドロックを完全に回避することもできます。

170
SLaks

同期コードからasyncコードを呼び出すのはかなり面倒です。

私は説明します 私のブログでこのデッドロックの完全な理由 。つまり、デフォルトで各awaitの先頭に保存され、メソッドを再開するために使用される「コンテキスト」があります。

したがって、これがUIコンテキストで呼び出された場合、awaitが完了すると、asyncメソッドはそのコンテキストを再入力して実行を継続しようとします。残念ながら、Wait(またはResult)を使用するコードはそのコンテキストでスレッドをブロックするため、asyncメソッドは完了できません。

これを回避するためのガイドラインは次のとおりです。

  1. できる限りConfigureAwait(continueOnCapturedContext: false)を使用してください。これにより、コンテキストを再入力しなくても、asyncメソッドを実行し続けることができます。
  2. asyncをずっと使用してください。 awaitまたはResultの代わりにWaitを使用してください。

もしあなたのメソッドが本来非同期であれば、 あなたは(おそらく)同期ラッパーを公開すべきではありません

46
Stephen Cleary

これが私がしたことです

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

うまく動作し、UIスレッドをブロックしていない

5
pixel

小さなカスタム同期コンテキストでは、同期機能は、デッドロックを発生させることなく、非同期機能の完了を待つことができます。これが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
0
codefox