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

本文探讨了在Windows Store Apps中编写日志记录器时遇到的问题,涉及异步和同步方法。当同步方法等待异步操作时,程序可能会冻结。原因在于异步方法内部的任务试图回到UI线程,但由于UI线程正忙于等待任务完成,导致死锁。解决方案是确保异步操作在非UI线程上运行或使用ConfigureAwait(false)避免返回特定上下文。
摘要由CSDN通过智能技术生成

本文翻译自: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: 避免这种情况的准则是:

  1. Use ConfigureAwait(continueOnCapturedContext: false) as much as possible. 尽可能使用ConfigureAwait(continueOnCapturedContext: false) This enables your async methods to continue executing without having to re-enter the context. 这使您的async方法可以继续执行而不必重新输入上下文。
  2. Use async all the way. 一路使用async Use await instead of Result or Wait . 使用await代替ResultWait

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值