web-dev-qa-db-ja.com

非同期メソッドでデッドロックが待機するのを避けるために、同期メソッドでTask.Run()を使用しますか?

[〜#〜] update [〜#〜]この質問の目的は、Task.Run()およびデッドロックに関する簡単な答えを得ることです。私は非同期と同期を混ぜないことの理論的理由をよく理解しており、それらを心に留めています。私は他の人から新しいことを学ぶ以上のものではありません。できるときはいつでもそうするよう努めています。技術的な答えだけが必要な場合があります...

非同期メソッドを呼び出す必要があるDispose()メソッドがあります。私のコードの95%は非同期であるため、リファクタリングは最良の選択ではありません。フレームワークでサポートされているIAsyncDisposable(他の機能の中でも)が理想的ですが、まだありません。そのため、当面は、デッドロックなしで同期メソッドから非同期メソッドを呼び出すための信頼できる方法を見つける必要があります。

notを使用してConfigureAwait(false)を使用することをお勧めします。これにより、呼び出し元が同期している場合に、呼び出し先が特定の動作をするようにコード全体に責任が分散されるためです。それは逸脱した盗聴者であるため、同期メソッドで何かをすることを好む。

Task.Run()は常に非同期メソッドでもスレッドプールをスケジュールするという別の質問でStephen Clearyのコメントを読んだ後、私は考えさせられました。

ASP.NETの.NET 4.5または現在のスレッド/同じスレッドにタスクをスケジュールする他の同期コンテキスト(非同期メソッドがある場合):

_private async Task MyAsyncMethod()
{
    ...
}
_

非同期メソッドをスレッドプールにキューイングするため、同期メソッドから呼び出したいのですが、Task.Run()Wait()と一緒に使用してデッドロックを回避できますか?

_private void MySynchronousMethodLikeDisposeForExample()
{
    // MyAsyncMethod will get queued to the thread pool 
    // so it shouldn't deadlock with the Wait() ??
    Task.Run((Func<Task>)MyAsyncMethod).Wait();
}
_
39
MikeJansen

あなたはあなたの質問に伴うリスクを理解しているようですので、私は講義をスキップします。

実際の質問に答えるには:はい、Task.Runを使用して、その作業をThreadPoolのないSynchronizationContextスレッドにオフロードすることができます。したがって、デッドロックの本当のリスクはありません。

ただし、SCがないという理由だけで別のスレッドを使用するのは、ハッキングのようなものであり、ThreadPoolで実行する作業のスケジューリングにはコストがかかるため、コストがかかる可能性があります。

IMOのより優れた明確なソリューションは、SCを一時的に削除して、SynchronizationContext.SetSynchronizationContextを使用し、後で復元することです。これはIDisposableに簡単にカプセル化できるため、usingスコープで使用できます。

public static class NoSynchronizationContextScope
{
    public static Disposable Enter()
    {
        var context = SynchronizationContext.Current;
        SynchronizationContext.SetSynchronizationContext(null);
        return new Disposable(context);
    }

    public struct Disposable : IDisposable
    {
        private readonly SynchronizationContext _synchronizationContext;

        public Disposable(SynchronizationContext synchronizationContext)
        {
            _synchronizationContext = synchronizationContext;
        }

        public void Dispose() =>
            SynchronizationContext.SetSynchronizationContext(_synchronizationContext);
    }
}

使用法:

private void MySynchronousMethodLikeDisposeForExample()
{
    using (NoSynchronizationContextScope.Enter())
    {
        MyAsyncMethod().Wait();
    }
}
56
i3arnon

これは、非同期メソッドを同期的に呼び出す必要があり、スレッドがUIスレッドである場合にデッドロックを回避する私の方法です。

    public static T GetResultSafe<T>(this Task<T> task)
    {
        if (SynchronizationContext.Current == null)
            return task.Result;

        if (task.IsCompleted)
            return task.Result;

        var tcs = new TaskCompletionSource<T>();
        task.ContinueWith(t =>
        {
            var ex = t.Exception;
            if (ex != null)
                tcs.SetException(ex);
            else
                tcs.SetResult(t.Result);
        }, TaskScheduler.Default);

        return tcs.Task.Result;
    }
3
Dmitry Naumov

このコードは、あなたが質問で強調した正確な理由でデッドロックしません-コードは常に同期コンテキストなしで実行され(スレッドプールを使用するため)、Waitは単にスレッドtill/ifメソッドが戻ることをブロックします。

3
Alexei Levenkov

小さなカスタム同期コンテキストを使用すると、同期関数は、デッドロックを作成することなく、非同期関数の完了を待つことができます。元のスレッドは保持されるため、syncメソッドは、非同期関数の呼び出しの前後に同じスレッドを使用します。 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
1
codefox

must同期メソッドからasyncメソッドを絶対に呼び出す場合は、非同期メソッド呼び出し内でConfigureAwait(false)を使用して、同期コンテキストのキャプチャを回避してください。

これは保持されるはずですが、せいぜい不安定です。リファクタリングを考えることをお勧めします。代わりに。

1
Yuval Itzchakov