新文章 网摘 文章 随笔 日记

Await, SynchronizationContext, and Console Apps: Part 3

Stephen Toub - MSFT

 

在这个简短系列的第1 部分和第2 部分中,我演示了如何构建 SynchronizationContext 并使用它来运行异步方法,以便该方法中的所有延续都将在当前线程上序列化时运行。在控制台应用中或在不直接支持异步方法的单元测试框架中执行异步方法时,这可能很有用。但是,到目前为止,我展示的支持针对返回 Task 的异步方法...返回 void 的异步方法呢?

C# 和 Visual Basic 支持两种类型的异步方法:返回任务(任务或任务<T>)的方法和返回 void 的方法。前者使用返回的 Task 来表示异步方法的完成。但是,在“异步 void”方法的情况下,没有返回的 Task 来表示该方法的处理。相反,“异步无效”方法与当前同步上下文交互,以提醒上下文注意异步方法的执行状态。在进入异步方法的主体之前,如果存在当前同步上下文,则会检索它并调用其 OperationStarted 方法。异步方法完成后,同一上下文将调用其 OperationCompleted 方法。此外,如果异常在异步 void 方法的主体中未处理,则引发该异常将发布到 SynchronizationContext,以便异常转义回上下文以供其随意处理。

所有这些都意味着,如果我们希望我们的 AsyncPump 能够处理“异步 void”方法以及“异步任务”方法,我们需要稍微增加类型。首先,我们需要增强我们的 SingleThreadSynchronizationContext,以便对 OperationStarted 和 OperationDone 的调用做出适当的反应。这些方法需要维护有多少未完成操作的计数,以便当计数达到 0 时,我们调用 Complete,就像以前在异步方法的任务完成时调用 Complete 一样。为此,我们将三个成员添加到自定义上下文中:

private int m_operationCount = 0;

public override void OperationStarted()
{
    Interlocked.Increment(ref m_operationCount);
}

public override void OperationCompleted()
{
    if (Interlocked.Decrement(ref m_operationCount) == 0)
        Complete();
}

然后我们需要添加一个新的 AsyncPump.Run 重载,它适用于 Action(对于“异步无效”方法)而不是 Func<Task>(对于“异步任务”方法)。提醒一下,下面是 AsyncPump 类中的现有 Run 方法:

public static void Run(Func<Task> asyncMethod)
{
    var prevCtx = SynchronizationContext.Current;
    try
    {
        var syncCtx = new SingleThreadSynchronizationContext(false);
        SynchronizationContext.SetSynchronizationContext(syncCtx);

        var t = asyncMethod();
        t.ContinueWith(delegate { syncCtx.Complete(); }, TaskScheduler.Default);

        syncCtx.RunOnCurrentThread();
        t.GetAwaiter().GetResult();
    }
    finally
    {
        SynchronizationContext.SetSynchronizationContext(prevCtx);
    }
}

对于我们新的基于操作的变体,其中大部分将保持不变。事实上,对于新的,我们主要只需要删除与返回的任务有关的所有代码,因为没有一个,并且所有完成的概念都由我们添加到上下文中的 OperationStarted 和 OperationCompleted 方法处理。我们确实通过调用 OperationStarted 和 OperationFinish 来包围 asyncMethod 调用,以防万一 asyncMethod 实际上只是一个 void 方法而不是“async void”方法,在这种情况下,我们需要确保在调用期间操作计数大于 0,以避免在委托调用其他异步 void 方法时可能导致的竞争。

public static void Run(Action asyncMethod)
{
    var prevCtx = SynchronizationContext.Current;
    try
    {
        var syncCtx = new SingleThreadSynchronizationContext(true);
        SynchronizationContext.SetSynchronizationContext(syncCtx);

        syncCtx.OperationStarted();
        asyncMethod();
        syncCtx.OperationCompleted();

        syncCtx.RunOnCurrentThread();
    }
    finally
    {
        SynchronizationContext.SetSynchronizationContext(prevCtx);
    }
}

就是这样(请注意,我已经在 SingleThreadSynchronizationContext 的构造函数中添加了一个参数,它允许我指定是否应执行操作计数跟踪:我们希望它用于这个新的 Run 方法,但不适用于前面描述的方法)。我们现在能够使用我们的 AsyncPump 与当前线程上执行的所有延续同步运行“异步 void”方法,例如

static void Main()
{
    AsyncPump.Run((Action)FooAsync);
}

static async void FooAsync()
{
    Foo1Async();
    await Foo2Async();
    Foo3Async();
}

static async void Foo1Async()
{
    await Task.Delay(1000);
    Console.WriteLine(1);
}

static async Task Foo2Async()
{
    await Task.Delay(1000);
    Console.WriteLine(2);
}

static async void Foo3Async()
{
    await Task.Delay(1000);
    Console.WriteLine(3);
}

快乐异步。

AsyncPump.cs

 

Await, SynchronizationContext, and Console Apps: Part 3 - .NET Parallel Programming (microsoft.com)

posted @ 2022-11-09 09:49  岭南春  阅读(42)  评论(0)    收藏  举报