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


318

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

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

我正在为Windows Store Apps编写记录器,并且希望同时支持异步和同步记录。异步方法遵循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 2013年

3
@amadib,第1版和第4版在[提供的答案中进行了解释。Ver.2和3尝试重新启动已启动的任务。发表您的问题。尚不清楚如何在.NET 4.0上出现.NET 4.5异步/等待问题
Gennady VaninГеннадийВанин

1
版本4是Xamarin Forms的最佳选择。我们尝试了其余的选择,并且在所有情况下均未解决并遇到僵局
Ramakrishna

谢谢!第4版对我有用。但是它仍然异步运行吗?我假设是因为存在async关键字。
sshirley

Answers:


189

await您的异步方法内试图回到UI线程。

由于UI线程正忙于等待整个任务完成,因此您将陷入僵局。

移动异步调用即可Task.Run()解决问题。
由于异步调用现在正在线程池线程上运行,因此它不会尝试返回UI线程,因此一切正常。

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


9
+1。这是另一种解释- 等待,UI和死锁!天啊!
Alexei Levenkov

13
ConfigureAwait(false)是在这种情况下,适当的解决方案。由于它不需要在捕获的上下文中调用回调,因此它不需要。作为一种API方法,它应该在内部处理它,而不是强迫所有调用者移出UI上下文。
Servy

@Servy自从您提到ConfigureAwait以来一直在询问。我正在使用.net3.5,我不得不删除configure await cos,它在我正在使用的异步库中不可用。我该如何编写自己的消息,或者还有另一种等待异步呼叫的方法。因为我的方法也挂了。我没有Task,但没有Task.Run。这应该是一个单独的问题。
flexxxit

@flexxxit:您应该使用Microsoft.Bcl.Async
SLaks

48

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

在博客上解释了造成这种僵局全部原因。简而言之,每个上下文的开头都有一个默认保存的“上下文”,await用于恢复该方法。

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

避免这种情况的准则是:

  1. 使用ConfigureAwait(continueOnCapturedContext: false)尽可能多地。这使您的async方法可以继续执行而不必重新输入上下文。
  2. 一直使用async。使用await代替ResultWait

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


我需要在catch()中执行一个异步任务,该任务不支持async我该怎么做并防止失火和忘记情况。
Zapnologica

1
@Zapnologica:从VS2015开始awaitcatch块支持。如果您使用的是旧版本,则可以将异常分配给局部变量,await然后在catch块之后执行
史蒂芬·克利西

5

这是我所做的

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.