357

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

尽管花了几天时间研究有关基于任务的异步模式 (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 导致冻结?任务对象有问题吗?是否存在不明显的死锁?

4

5 回答 5

214

您的await异步方法内部正在尝试返回 UI 线程。

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

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

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

于 2013-01-23T16:59:06.310 回答
54

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

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

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

避免这种情况的准则是:

  1. ConfigureAwait(continueOnCapturedContext: false)尽可能多地使用。这使您的async方法能够继续执行而无需重新进入上下文。
  2. async一路使用。使用await代替Resultor Wait

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

于 2013-01-23T17:08:56.320 回答
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();
}

工作得很好,不会阻塞 UI 线程

于 2016-10-25T23:34:24.877 回答
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
于 2016-09-27T13:27:06.827 回答
-1

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

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

也可以在UI-Content没有阻塞和调度程序问题的情况下工作,也可以来自 CTOR。

于 2021-06-18T16:27:25.913 回答