同步等待异步操作,为什么在此使用Wait()会导致程序冻结

378

前言:我正在寻找一种解释,而不仅仅是一个解决方案。我已经知道了解决方案。

尽管花费了几天时间学习有关任务异步模式(TAP)、async和await的MSDN文章,但我仍对某些细节感到有些困惑。

我正在为Windows Store应用程序编写日志记录器,并且希望支持异步和同步记录。异步方法遵循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();
}

这看起来是正确的,但它并不起作用。整个程序会永久冻结。

版本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.

第三个版本:

嗯...Task.RunSynchronously听起来很有前途。

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

这会抛出 InvalidOperationException 异常:无法在未绑定委托的任务上调用 RunSynchronously 方法,例如从异步方法返回的任务。

版本 4(解决方案):

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

这个有效。因此,2和3是错误的工具。但是1呢?1有什么问题,与4有什么区别?是什么导致1会导致程序冻结?任务对象存在问题吗?是否存在非明显的死锁?


1
有没有在其他地方找到解释的运气?下面的答案并没有提供深入的见解。我实际上正在使用的是 .net 4.0 而不是 4.5/5,所以我无法使用一些操作,但遇到了相同的问题。 - amadib
3
@amadib,版本1和4在[提供的答案中已经解释了。版本2和3尝试重新开始已经开始的任务。请发布您的问题。不清楚您如何在.NET 4.0上遇到.NET 4.5异步/等待问题。 - Gennady Vanin Геннадий Ванин
2
版本4是Xamarin Forms的最佳选择。我们尝试了其他选项,但都没有起作用,并且在所有情况下都遇到了死锁问题。 - Ramakrishna
谢谢!第四个版本对我有用。但它仍然是异步运行的吗?我假设是因为有async关键字。 - sshirley
5个回答

226
您异步方法内的await语句试图返回到UI线程。
由于UI线程正忙于等待整个任务完成,因此产生了死锁。
将异步调用移至Task.Run()可以解决此问题。 因为异步调用现在在线程池线程上运行,它不会尝试返回到UI线程,因此一切都可以正常工作。
或者,在等待内部操作之前调用StartAsTask().ConfigureAwait(false),使其返回到线程池而不是UI线程,从而完全避免死锁。

11
这是一篇关于"等待、UI和死锁"的文章,作者讲解了在异步编程中如何避免出现死锁问题,特别是在涉及到UI线程的情况下。文章详细介绍了await操作符、TaskScheduler.FromCurrentSynchronizationContext方法以及AsyncLock类的使用方法,这些都可以用来确保异步代码不会阻塞UI线程并避免死锁问题的发生。 - Alexei Levenkov
14
在这种情况下,ConfigureAwait(false)是合适的解决方案。由于没有必要在捕获的上下文中调用回调函数,因此不应该这样做。作为一个API方法,它应该在内部处理,而不是强制所有调用者移出UI上下文。 - Servy
@flexxxit:你应该使用Microsoft.Bcl.Async - SLaks
1
@AlexeiLevenkov 的链接已经失效了。 - DoobieAsDave
1
这是更新后的链接:等待、UI和死锁!哦,天哪! - Ant
显示剩余2条评论

54

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

我在我的博客中解释了 这个死锁的完整原因。简而言之,每个 await 的开头都默认保存一个“上下文”,用于恢复该方法。

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

避免这种情况的指南是:

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

如果您的方法自然是异步的,那么 您(可能)不应该公开同步包装器


我需要在不支持async的catch()中执行一个异步任务,我该如何做到这一点并避免出现“fire and forget”的情况。 - Zapnologica
2
@Zapnologica:自VS2015起,awaitcatch块中得到支持。如果您使用的是旧版本,则可以将异常分配给本地变量,并在catch块之后执行await(http://blog.stephencleary.com/2014/06/await-in-catch-and-finally.html)。 - Stephen Cleary

7

以下是我的操作记录:

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();
}

非常顺畅,不会阻塞用户界面


我尝试了这段代码但它不起作用。我可以使用这段代码同步调用方法吗? - Shashank Singh

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

0

对我来说,实际上最好的工作解决方案是:

AsyncMethod(<params>).ConfigureAwait(true).GetAwaiter().GetResult();

可以在UI-Content上工作,而且不会阻塞和调度问题,也可以从CTOR中使用。


дљ†еПѓдї•зЬБзХ•ConfigureAwait(true)пЉМеЫ†дЄЇtrueжШѓйїШиЃ§еПВжХ∞гАВ - Zoner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接