Использовать Task.Run() в синхронном методе, чтобы избежать ожидания тупика при асинхронном методе?
ОБНОВЛЕНИЕ Цель этого вопроса - получить простой ответ о Task.Run()
и блокировка. Я очень хорошо понимаю теоретические рассуждения о том, что нельзя смешивать асинхронность и синхронизацию, и я принимаю их близко к сердцу. Я не выше изучения новых вещей от других; Я стараюсь делать это, когда могу. Есть моменты, когда парню нужен только технический ответ...
у меня есть Dispose()
метод, который должен вызвать асинхронный метод. Поскольку 95% моего кода является асинхронным, рефакторинг - не лучший выбор. Имея IAsyncDisposable
(среди прочих функций), который поддерживается фреймворком, был бы идеальным, но мы еще не там. Итак, в то же время мне нужно найти надежный способ вызова асинхронных методов из синхронного метода без блокировки.
Я бы предпочел не использовать ConfigureAwait(false)
потому что это оставляет ответственность, рассредоточенную по всему моему коду, для вызывающего, чтобы он вел себя определенным образом на тот случай, если вызывающий является синхронным. Я бы предпочел сделать что-то в синхронном методе, так как это отклоняющийся от нормы.
Прочитав комментарий Стивена Клири в другом вопросе, Task.Run()
я всегда думаю о пуле потоков, даже о асинхронных методах.
В.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();
}
5 ответов
Кажется, вы понимаете риски, связанные с вашим вопросом, поэтому я пропущу лекцию.
Чтобы ответить на ваш актуальный вопрос: Да, вы можете просто использовать 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();
}
}
Это мой способ избежать тупика, когда мне приходится синхронно вызывать асинхронный метод, а поток может быть потоком пользовательского интерфейса:
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;
}
Этот код не будет тупиковым по точным причинам, которые вы указали в вопросе - код всегда выполняется без контекста синхронизации (поскольку используется пул потоков) и Wait
просто заблокирует поток до / если метод вернется.
Если вам абсолютно необходимо вызвать асинхронный метод из синхронного, обязательно используйте ConfigureAwait(false)
внутри ваших вызовов асинхронных методов, чтобы избежать захвата контекста синхронизации.
Это должно держаться, но в лучшем случае шатко. Я бы посоветовал подумать о рефакторинге. вместо.
С небольшим настраиваемым контекстом синхронизации функция синхронизации может ожидать завершения асинхронной функции, не создавая взаимоблокировку. Исходный поток сохраняется, поэтому метод синхронизации использует один и тот же поток до и после вызова асинхронной функции. Вот небольшой пример для приложения 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