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

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

我正在为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:在promise类型的任务上不能调用Start。

版本3:

嗯. .的任务。runsynchrontically听起来很有希望。

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

这将引发InvalidOperationException:对于未绑定到委托的任务,例如异步方法返回的任务,不能调用runsynchronically。

版本4(解决方案):

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

这个作品。所以,2和3是错误的工具。但1 ?1和4有什么不同?是什么导致1结冰?任务对象是否有问题?是否存在不明显的死锁?


当前回答

从同步代码调用异步代码可能相当棘手。

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

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

避免这种情况的指导方针是:

尽量使用ConfigureAwait(continueOnCapturedContext: false)。这使得您的异步方法可以继续执行,而不必重新进入上下文。 始终使用async。使用await而不是Result或Wait。

如果方法本身是异步的,那么(可能)就不应该公开同步包装器。

其他回答

以下是我所做的

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

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

从同步代码调用异步代码可能相当棘手。

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

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

避免这种情况的指导方针是:

尽量使用ConfigureAwait(continueOnCapturedContext: false)。这使得您的异步方法可以继续执行,而不必重新进入上下文。 始终使用async。使用await而不是Result或Wait。

如果方法本身是异步的,那么(可能)就不应该公开同步包装器。

对我来说,最好的解决方案是:

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

工作也在ui内容没有阻塞和调度器的问题,也从CTOR的。

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

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

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

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