首页 文章

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

提问于
浏览
267

Preface :我正在寻找解释,而不仅仅是解决方案 . 我已经知道了解决方案 .

尽管花了几天时间研究MSDN关于基于任务的异步模式(TAP),异步和等待的文章,但我仍然对一些更精细的细节感到困惑 .

我正在为Windows Store应用程序编写 Logger ,我想支持异步和同步日志记录 . 异步方法遵循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);
}

现在相应的同步方法......

Version 1

private void WriteToLog(string text)
{
    Task task = WriteToLogAsync(text);
    task.Wait();
}

这看起来是正确的,但它不起作用 . 整个程序永远冻结 .

Version 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.

Version 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.

Version 4 (the solution):

private void WriteToLog(string text)
{
    var task = Task.Run(async () => { await WriteToLogAsync(text); });
    task.Wait();
}

这很有效 . 所以,2和3是错误的工具 . 但1? 1有什么不对,4有什么不同? 1导致冻结的原因是什么?任务对象有问题吗?是否存在非明显的死锁?

请帮我理解 .

4 回答

  • 45

    异步方法中的 await 试图回到UI线程 .

    由于UI线程忙于等待整个任务完成,因此您遇到了死锁 .

    将异步调用移至 Task.Run() 可解决此问题 .
    因为异步调用现在在线程池线程上运行,所以它不会尝试返回到UI线程,因此一切正常 .

    或者,您可以在等待内部操作之前调用 StartAsTask().ConfigureAwait(false) 以使其返回到线程池而不是UI线程,从而完全避免死锁 .

  • 156

    从同步代码调用 async 代码可能非常棘手 .

    我解释full reasons for this deadlock on my blog . 简而言之,默认情况下会在 await 的开头保存"context"并用于恢复该方法 .

    因此,如果在UI上下文中调用此方法,则 await 完成时, async 方法会尝试重新进入该上下文以继续执行 . 不幸的是,使用 Wait (或 Result )的代码将阻止该上下文中的线程,因此 async 方法无法完成 .

    避免这种情况的指导原则是:

    • 尽可能使用 ConfigureAwait(continueOnCapturedContext: false) . 这使您的 async 方法可以继续执行而无需重新输入上下文 .

    • 一直使用 async . 使用 await 而不是 ResultWait .

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

  • 4

    这就是我做的

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

  • 0

    使用小型自定义同步上下文,同步功能可以等待异步功能的完成,而不会产生死锁 . 这是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
    

相关问题