同步等待一个异步操作,为啥Wait()会在这里冻结程序
Posted
技术标签:
【中文标题】同步等待一个异步操作,为啥Wait()会在这里冻结程序【英文标题】:Synchronously waiting for an async operation, and why does Wait() freeze the program here同步等待一个异步操作,为什么Wait()会在这里冻结程序 【发布时间】:2013-01-07 06:01:34 【问题描述】:前言:我正在寻找解释,而不仅仅是解决方案。我已经知道解决方案了。
尽管我花了几天时间研究有关基于任务的异步模式 (TAP)、async 和 await 的 MSDN 文章,但我仍然对一些更精细的细节感到有些困惑。
我正在为 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 导致冻结?任务对象有问题吗?是否存在不明显的死锁?
【问题讨论】:
运气好能在别处得到解释吗?下面的答案真的没有提供洞察力。我实际上使用的是 .net 4.0 而不是 4.5/5,所以我不能使用某些操作但遇到了同样的问题。 @amadib,ver.1 和 4 在 [rpvided answers 中进行了解释。 Ver.2 anв 3 尝试重新开始已经开始的任务。发布您的问题。目前尚不清楚如何在 .NET 4.0 上遇到 .NET 4.5 异步/等待问题 版本 4 是 Xamarin Forms 的最佳选择。我们尝试了其余的选项,但在所有情况下都没有成功并遇到死锁 谢谢!第 4 版对我有用。但它仍然异步运行吗?我假设是因为存在 async 关键字。 【参考方案1】:您的异步方法中的 await
正在尝试返回 UI 线程。
由于 UI 线程正忙于等待整个任务完成,因此出现了死锁。
将异步调用移至 Task.Run()
即可解决问题。
因为异步调用现在在线程池线程上运行,它不会尝试返回 UI 线程,因此一切正常。
或者,您可以在等待内部操作之前调用 StartAsTask().ConfigureAwait(false)
使其返回线程池而不是 UI 线程,从而完全避免死锁。
【讨论】:
+1。这里还有一个解释-Await, and UI, and deadlocks! Oh my!ConfigureAwait(false)
在这种情况下是合适的解决方案。因为它不需要在捕获的上下文中调用回调,所以它不应该。作为一个 API 方法,它应该在内部处理它,而不是强制所有调用者移出 UI 上下文。
@Servy 我在问,因为你提到了 ConfigureAwait。我正在使用.net3.5,我不得不删除配置等待因为它在我使用的异步库中不可用。我如何编写自己的,或者是否有另一种等待我的异步调用的方式。因为我的方法也挂了。我没有Task,但没有Task.Run。这可能本身就是一个问题。
@flexxxit:你应该使用Microsoft.Bcl.Async
。
@AlexeiLevenkov 链接已失效。【参考方案2】:
从同步代码中调用async
代码可能非常棘手。
我解释full reasons for this deadlock on my blog。简而言之,每个await
的开头默认保存一个“上下文”,用于恢复方法。
因此,如果在 UI 上下文中调用它,当 await
完成时,async
方法会尝试重新进入该上下文以继续执行。不幸的是,使用Wait
(或Result
)的代码将阻塞该上下文中的线程,因此async
方法无法完成。
避免这种情况的准则是:
-
尽可能使用
ConfigureAwait(continueOnCapturedContext: false)
。这使您的 async
方法可以继续执行,而无需重新进入上下文。
一直使用async
。使用await
而不是Result
或Wait
。
如果你的方法自然是异步的,那么you (probably) shouldn't expose a synchronous wrapper。
【讨论】:
我需要在不支持async
的 catch() 中执行异步任务,我将如何做到这一点并防止发生火灾和忘记的情况。
@Zapnologica: await
在 VS2015 的 catch
块中得到支持。如果您使用的是旧版本,you can assign the exception to a local variable and do the await
after the catch block。【参考方案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();
运行良好且不阻塞 UI 线程
【讨论】:
【参考方案4】:使用小的自定义同步上下文,同步函数可以等待异步函数完成,而不会造成死锁。这是 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】:对我来说实际上是最好的解决方案:
AsyncMethod(<params>).ConfigureAwait(true).GetAwaiter().GetResult();
也适用于 UI-Content
,没有阻塞和调度程序问题,也适用于 CTOR。
【讨论】:
以上是关于同步等待一个异步操作,为啥Wait()会在这里冻结程序的主要内容,如果未能解决你的问题,请参考以下文章