本文翻译自:Synchronously waiting for an async operation, and why does Wait() freeze the program here
Preface : I'm looking for an explanation, not just a solution. 前言 :我在寻找一个解释,而不仅仅是一个解决方案。 I already know the solution. 我已经知道了解决方案。
Despite having spent several days studying MSDN articles about the Task-based Asynchronous Pattern (TAP), async and await, I'm still a bit confused about some of the finer details. 尽管花了几天时间研究有关基于任务的异步模式(TAP),异步和等待的MSDN文章,但我对某些更详细的信息仍然感到困惑。
I'm writing a logger for Windows Store Apps, and I want to support both asynchronous and synchronous logging. 我正在为Windows Store Apps编写记录器,并且希望同时支持异步和同步记录。 The asynchronous methods follow the TAP, the synchronous ones should hide all this, and look and work like ordinary methods. 异步方法遵循TAP,同步方法应该隐藏所有这些内容,并且外观和工作方式与普通方法类似。
This is the core method of asynchronous logging: 这是异步日志记录的核心方法:
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);
}
Now the corresponding synchronous method... 现在对应的同步方法...
Version 1 : 版本1 :
private void WriteToLog(string text)
{
Task task = WriteToLogAsync(text);
task.Wait();
}
This looks correct, but it does not work. 看起来正确,但是不起作用。 The whole program freezes forever. 整个程序永久冻结。
Version 2 : 版本2 :
Hmm.. Maybe the task was not started? 嗯..也许任务没有开始?
private void WriteToLog(string text)
{
Task task = WriteToLogAsync(text);
task.Start();
task.Wait();
}
This throws InvalidOperationException: Start may not be called on a promise-style task.
这将引发InvalidOperationException: Start may not be called on a promise-style task.
Version 3: 版本3:
Hmm.. Task.RunSynchronously
sounds promising. 嗯Task.RunSynchronously
听起来很有希望。
private void WriteToLog(string text)
{
Task task = WriteToLogAsync(text);
task.RunSynchronously();
}
This throws InvalidOperationException: RunSynchronously may not be called on a task not bound to a delegate, such as the task returned from an asynchronous method.
这将引发InvalidOperationException: RunSynchronously may not be called on a task not bound to a delegate, such as the task returned from an asynchronous method.
Version 4 (the solution): 版本4(解决方案):
private void WriteToLog(string text)
{
var task = Task.Run(async () => { await WriteToLogAsync(text); });
task.Wait();
}
This works. 这可行。 So, 2 and 3 are the wrong tools. 因此,2和3是错误的工具。 But 1? 但是1? What's wrong with 1 and what's the difference to 4? 1有什么问题,与4有什么区别? What makes 1 cause a freeze? 是什么导致1冻结? Is there some problem with the task object? 任务对象有问题吗? Is there a non-obvious deadlock? 有没有明显的僵局?
#1楼
参考:https://stackoom.com/question/ymEt/同步等待异步操作-为什么Wait-在这里冻结程序
#2楼
The await
inside your asynchronous method is trying to come back to the UI thread. 异步方法中的await
正在尝试返回UI线程。
Since the UI thread is busy waiting for the entire task to complete, you have a deadlock. 由于UI线程正忙于等待整个任务完成,因此出现了死锁。
Moving the async call to Task.Run()
solves the issue. 将异步调用移到Task.Run()
可以解决此问题。
Because the async call is now running on a thread pool thread, it doesn't try to come back to the UI thread, and everything therefore works. 由于异步调用现在正在线程池线程上运行,因此它不会尝试返回UI线程,因此一切正常。
Alternatively, you could call StartAsTask().ConfigureAwait(false)
before awaiting the inner operation to make it come back to the thread pool rather than the UI thread, avoiding the deadlock entirely. 或者,您可以在等待内部操作使它返回线程池而不是UI线程之前调用StartAsTask().ConfigureAwait(false)
,从而完全避免死锁。
#3楼
Calling async
code from synchronous code can be quite tricky. 从同步代码中调用async
代码可能非常棘手。
I explain the full reasons for this deadlock on my blog . 我在博客上解释了造成这种僵局的全部原因 。 In short, there's a "context" that is saved by default at the beginning of each await
and used to resume the method. 简而言之,有一个“上下文”默认情况下在每次await
的开始时保存,并用于恢复该方法。
So if this is called in an UI context, when the await
completes, the async
method tries to re-enter that context to continue executing. 因此,如果在UI上下文中调用此方法,则在await
完成时, async
方法将尝试重新输入该上下文以继续执行。 Unfortunately, code using Wait
(or Result
) will block a thread in that context, so the async
method cannot complete. 不幸的是,使用Wait
(或Result
)的代码将在该上下文中阻塞线程,因此async
方法无法完成。
The guidelines to avoid this are: 避免这种情况的准则是:
- Use
ConfigureAwait(continueOnCapturedContext: false)
as much as possible. 尽可能使用ConfigureAwait(continueOnCapturedContext: false)
。 This enables yourasync
methods to continue executing without having to re-enter the context. 这使您的async
方法可以继续执行而不必重新输入上下文。 - Use
async
all the way. 一路使用async
。 Useawait
instead ofResult
orWait
. 使用await
代替Result
或Wait
。
If your method is naturally asynchronous, then you (probably) shouldn't expose a synchronous wrapper . 如果您的方法自然是异步的,那么(可能)您不应该公开同步包装器 。
#4楼
With small custom synchronization context, sync function can wait for completion of async function, without creating deadlock. 使用小的自定义同步上下文,同步功能可以等待异步功能完成,而不会产生死锁。 Here is small example for WinForms app. 这是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
#5楼
Here is what I did 这是我所做的
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();
}
working great and not blocking UI thread 工作正常,不阻塞UI线程