Подтвердить что ты не робот

Использовать Task.Run() в синхронном методе, чтобы избежать тупиковой остановки в асинхронном методе?

ОБНОВЛЕНИЕ Цель этого вопроса - получить простой ответ о Task.Run() и тупиковой ситуации. Я очень понимаю теоретические рассуждения о том, что вы не смешиваете асинхронные и синхронизирующие сигналы, и я принимаю их близко к сердцу. Я не выше изучения новых вещей от других; Я стараюсь делать это, когда смогу. Там просто раз, когда нужен весь парень, это технический ответ...

У меня есть метод Dispose(), который должен вызвать метод async. Поскольку 95% моего кода является асинхронным, рефакторинг не лучший выбор. Наличие IAsyncDisposable (среди других функций), которое поддерживалось каркасом, было бы идеальным, но мы еще не были там. Поэтому в то же время мне нужно найти надежный способ вызова асинхронных методов из синхронного метода без блокировки.

Я бы предпочел не использовать ConfigureAwait(false), потому что это оставляет ответственность, разбросанную по всему моему коду, чтобы вызываемый мог вести себя определенным образом, только если вызывающий является синхронным. Я бы предпочел сделать что-то в синхронном методе, так как это девиантный bugger.

После прочтения слова Стивена Клири в другом вопросе, который Task.Run() всегда расписывает в пуле потоков даже методы async, это заставило меня подумать.

В .NET 4.5 в ASP.NET или в любом другом контексте синхронизации, который планирует задачи для текущего потока/одного потока, если у меня есть асинхронный метод:

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

Ответ 1

Кажется, вы понимаете риски, связанные с вашим вопросом, поэтому я пропущу лекцию.

Чтобы ответить на ваш реальный вопрос: да, вы можете просто использовать 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();
    }
}

Ответ 2

Этот код не будет тупиком по тем причинам, которые вы выделили в вопросе - код всегда работает без контекста синхронизации (с тех пор, как использует пул потоков) и Wait будет просто блокировать поток до/если метод возвращает.

Ответ 3

Это мой способ избежать тупиковой ситуации, когда мне приходится синхронно вызывать метод асинхронного потока, а поток может быть потоком пользовательского интерфейса:

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

Ответ 4

Если вы абсолютно должны вызывать метод async из синхронного, обязательно используйте ConfigureAwait(false) внутри вызовов метода async, чтобы избежать захвата контекста синхронизации.

Это должно держаться, но в лучшем случае шатко. Я бы посоветовал подумать о рефакторинге.  вместо этого.

Ответ 5

С небольшим пользовательским контекстом синхронизации функция синхронизации может ждать завершения функции async, не создавая тупика. Исходный поток сохраняется, поэтому метод синхронизации использует один и тот же поток до и после вызова функции async. Вот небольшой пример для приложения 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