【问题标题】: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 关键字。

标签: c# .net task-parallel-library windows-store-apps async-await


【解决方案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 方法无法完成。

避免这种情况的准则是:

  1. 尽可能使用ConfigureAwait(continueOnCapturedContext: false)。这使您的 async 方法可以继续执行,而无需重新进入上下文。
  2. 一直使用async。使用await 而不是ResultWait

如果你的方法自然是异步的,那么you (probably) shouldn't expose a synchronous wrapper

【讨论】:

【解决方案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。

      【讨论】:

        猜你喜欢
        • 2013-03-17
        • 2014-05-15
        • 1970-01-01
        • 2021-11-21
        • 2014-08-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多