同步等待一个异步操作,为什么Wait()在这里冻结了程序

前言:我在寻找一个解释,而不仅仅是一个解决方案。我已经知道解了。

尽管花了几天时间研究MSDN上关于基于任务的异步模式(TAP)、async和await的文章,但我仍然对一些更精细的细节感到困惑。

我正在为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结冰?任务对象是否有问题?是否存在不明显的死锁?

138715 次浏览

异步方法中的await正在尝试返回到UI线程。

由于UI线程忙于等待整个任务完成,因此出现了死锁。

将异步调用移动到Task.Run()可以解决问题 因为异步调用现在运行在线程池线程上,它不会尝试返回到UI线程,因此一切都可以正常工作

或者,你可以在等待内部操作之前调用StartAsTask().ConfigureAwait(false),让它回到线程池而不是UI线程,从而完全避免死锁。

从同步代码调用async代码可能相当棘手。

我解释了在我的博客上有这个僵局的全部原因。简而言之,在每个await的开头默认保存了一个“context”,用于恢复该方法。

因此,如果在UI上下文中调用此方法,当await完成时,async方法将尝试重新进入该上下文中以继续执行。不幸的是,使用Wait(或Result)的代码将阻塞该上下文中的线程,因此async方法无法完成。

避免这种情况的指导方针是:

  1. 尽量使用ConfigureAwait(continueOnCapturedContext: false)。这使得你的async方法可以继续执行,而不必重新进入上下文。
  2. 始终使用async。使用await代替ResultWait

如果你的方法是自然异步的,那么您(可能)不应该公开同步包装器

使用小的自定义同步上下文,同步函数可以等待异步函数的完成,而不会产生死锁。下面是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

以下是我所做的

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线程

对我来说,最好的解决方案是:

AsyncMethod(<params>).ConfigureAwait(true).GetAwaiter().GetResult();

也工作在UI-Content没有阻塞和调度程序的问题,也从CTOR的。