Синхронно ждет асинхронной операции и почему Wait() заморозит программу здесь
Предисловие: я ищу объяснение, а не просто решение. Я уже знаю решение.
Несмотря на то, что я потратил несколько дней на изучение статей MSDN об асинхронном шаблоне на основе задач (TAP), асинхронности и ожидания, я все еще немного озадачен некоторыми деталями.
Я пишу регистратор для приложений Магазина 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 причиной замораживания? Есть ли проблема с объектом задачи? Есть ли неочевидный тупик?
Ответы
Ответ 1
await
внутри вашего асинхронного метода пытается вернуться к потоку пользовательского интерфейса.
Поскольку поток пользовательского интерфейса занят, ожидая завершения всей задачи, у вас есть тупик.
Перемещение асинхронного вызова в Task.Run()
решает проблему.
Поскольку асинхронный вызов теперь запущен в потоке пула потоков, он не пытается возвращаться к потоку пользовательского интерфейса, и поэтому все работает.
В качестве альтернативы вы можете вызвать StartAsTask().ConfigureAwait(false)
перед ожиданием внутренней операции, чтобы она возвращалась к пулу потоков, а не к потоку пользовательского интерфейса, полностью исключая тупик.
Ответ 2
Вызов кода async
из синхронного кода может быть довольно сложным.
Я объясняю полные причины этого тупика в моем блоге. Короче говоря, существует "контекст", который сохраняется по умолчанию в начале каждого await
и используется для возобновления метода.
Итак, если это вызывается в контексте пользовательского интерфейса, когда завершается await
, метод async
пытается повторно ввести этот контекст для продолжения выполнения. К сожалению, код с использованием Wait
(или Result
) будет блокировать поток в этом контексте, поэтому метод async
не может завершиться.
Рекомендации, которые следует избегать:
- Используйте
ConfigureAwait(continueOnCapturedContext: false)
как можно больше. Это позволяет вашим методам async
продолжать выполнение без повторного входа в контекст.
- Используйте
async
полностью. Используйте await
вместо Result
или Wait
.
Если ваш метод естественно асинхронен, то вы (вероятно) не должны выставлять синхронную обертку.
Ответ 3
Вот что я сделал
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();
}
работает отлично и не блокирует поток пользовательского интерфейса
Ответ 4
С небольшим пользовательским контекстом синхронизации функция синхронизации может ждать завершения функции 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