同步等待一个异步操作,为啥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 而不是ResultWait

如果你的方法自然是异步的,那么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()会在这里冻结程序的主要内容,如果未能解决你的问题,请参考以下文章

如何正确使用等待和通知方法?

java多线程(线程通信-等待换新机制-代码优化)

使用Task.Wait而不是等待异步编程

异步等待子处理?

java中5种异步转同步方法

java 异步通知和同步通知 啥意思 怎么处理