【深入浅出C# async/await】运行时上下文


前言

part1 - 【深入浅出C# async/await】编译篇
part2 - 【深入浅出C# async/await】理解 awaitable-awaiter 模式
part3 - 【深入浅出C# async/await】运行时上下文

在part1中解释了await的编译:

  • 在一个带有await关键字的异步方法中,所有的代码都被编译成一个状态机的MoveNext()方法。
  • 当调用这个异步方法时,状态机就会启动。随着状态的改变,MoveNext()会以类似回调的方式被调用。
internal static async Task<int> MultiCallMethodAsync(int arg0, int arg1, int arg2, int arg3)
{
    HelperMethods.Before();
    int resultOfAwait1 = await MethodAsync(arg0, arg1);
    HelperMethods.Continuation1(resultOfAwait1);
    int resultOfAwait2 = await MethodAsync(arg2, arg3);
    HelperMethods.Continuation2(resultOfAwait2);
    int resultToReturn = resultOfAwait1 + resultOfAwait2;
    return resultToReturn;
}

为了演示类似回调的机制,Part 1简单地使用了Task.ContinueWith()方法:

internal static Task<int> MultiCallMethodAsync(int arg0, int arg1, int arg2, int arg3)
{
    TaskCompletionSource<int> taskCompletionSource = new TaskCompletionSource<int>(); try {

    // Original code begins.
    HelperMethods.Before();
    // int resultOfAwait1 = await MethodAsync(arg0, arg1);
    MethodAsync(arg0, arg1).ContinueWith(await1 => { try { int resultOfAwait1 = await1.Result;
    HelperMethods.Continuation1(resultOfAwait1);
    // int resultOfAwait2 = await MethodAsync(arg2, arg3);
    MethodAsync(arg2, arg3).ContinueWith(await2 => { try { int resultOfAwait2 = await2.Result;
    HelperMethods.Continuation2(resultOfAwait2);
    int resultToReturn = resultOfAwait1 + resultOfAwait2;
    // return resultToReturn;
    taskCompletionSource.SetResult(resultToReturn);
    // Original code ends.

    } catch (Exception exception) { taskCompletionSource.SetException(exception); }});
    } catch (Exception exception) { taskCompletionSource.SetException(exception); }});
    } catch (Exception exception) { taskCompletionSource.SetException(exception); }
    return taskCompletionSource.Task;
}

实际上,await内部结构远比表面上看到的要复杂。


线程问题

可以使用一个微小的WPF应用程序进行一个简单的实验。它有一个带有文本框和按钮的窗口:

<Window x:Class="WpfAsync.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TextBox x:Name="TextBox" HorizontalAlignment="Left" Height="274" Margin="10,10,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="497"/>
        <Button x:Name="Button" Content="Button" HorizontalAlignment="Left" Margin="432,289,0,0" VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>
namespace WpfAsync
{
    using System.Net;

    public partial class MainWindow
    {
        public MainWindow()
        {
            this.InitializeComponent();
            this.Button.Click += async (sender, e) =>
            {
                string html = await new WebClient().DownloadStringTaskAsync("https://weblogs.asp.net/dixin");
                this.TextBox.Text = html;
            };
        }
    }
}

点击Button后,会异步地下载string.下载完成后,字符串会展示在TextBox中。
当然上面的代码可以运行。但是它如果用Task.ContinueWith()重写成回调的形式:

this.Button.Click += (sender, e) =>
{
    // string html = await new WebClient().DownloadStringTaskAsync("https://weblogs.asp.net/dixin");
    new WebClient().DownloadStringTaskAsync("https://weblogs.asp.net/dixin").ContinueWith(await => { string html = await.Result;
    this.TextBox.Text = html; });
};

运行上面的代码,continuation(this.TextBox.Text = html)会抛出InvalidOperationException:

The calling thread cannot access this object because a different thread owns it.

原因是,当回调代码被调度到线程池中的非UI线程时,它无法访问UI控件,比如改变TextBox的Text属性。在第一个async/await版本中,await基础设施通过将继续代码调回最初捕获的ExecutionContext和SynchronizationContext来解决跨线程问题。

Marshal To ExecutionContext

编排执行上下文
当将一堆代码重新调度到线程池中时(可能在另一个线程上),await的状态机调用机制会将初始调用线程的ExecutionContext传递给每个MoveNext()的下一个调用。正如MSDN所解释的那样:

The ExecutionContext class provides a single container for all information relevant to a logical thread of execution. This includes security context, call context, and synchronization context.
The ExecutionContext class provides the functionality for user code to capture and transfer this context across user-defined asynchronous points. The common language runtime ensures that the ExecutionContext is consistently transferred across runtime-defined asynchronous points within the managed process.

翻译
ExecutionContext类为逻辑执行线程提供了一个单一的容器,其中包括安全上下文、调用上下文和同步上下文。
ExecutionContext类提供了用户代码捕获和传输此上下文跨用户定义的异步点的功能。
公共语言运行时确保ExecutionContext在托管进程内的运行时定义的异步点之间始终得到传输。

这是捕获线程执行上下文的公共API:

// See: System.Runtime.CompilerServices.AsyncMethodBuilderCore.GetCompletionAction()
ExecutionContext executionContext = ExecutionContext.Capture();

下面的拓展方法说明了如何使用指定的ExecuntionContext(通常是从另一个线程同步过来的)调用函数:

public static class FuncExtensions
{
    public static TResult InvokeWith<TResult>(this Func<TResult> function, ExecutionContext executionContext)
    {
        Contract.Requires<ArgumentNullException>(function != null);

        if (executionContext == null)
        {
            return function();
        }

        TResult result = default(TResult);
        // See: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run()
        ExecutionContext.Run(executionContext, _ => result = function(), null);
        return result;
    }
}

Marshal To SynchronizationContext

await的内部实现也考虑到了同步上下文:

The SynchronizationContext class is a base class that provides a free-threaded context with no synchronization.
The purpose of the synchronization model implemented by this class is to allow the internal asynchronous/synchronous operations of the common language runtime to behave properly with different synchronization models.

在不同的环境下,SynchronizationContext有不同的实现,在.NET中如下:

  • WPF: System.Windows.Threading.DispatcherSynchronizationContext (这篇文章中)
  • WinForms: System.Windows.Forms.WindowsFormsSynchronizationContext
  • WinRT: System.Threading.WinRTSynchronizationContext
  • ASP.NET: System.Web.AspNetSynchronizationContext
    etc。

与 ExecutionContext 类似,状态机调用机制捕获初始 SynchronizationContext,并将 MoveNext() 的每次调用发送到该 SynchronizationContext。

这是捕获当前线程 SynchronizationContext 的公共 API:

// See: System.Runtime.CompilerServices.AsyncVoidMethodBuilder.Create()
// See: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run()
SynchronizationContext synchronizationContext = SynchronizationContext.Current;

此扩展方法演示了如何使用指定的 SynchronizationContext 和 ExecutionContext 调用函数:

public static class FuncExtensions
{
    public static Task<TResult> InvokeWith<TResult>(this Func<TResult> function, SynchronizationContext synchronizationContext, ExecutionContext executionContext)
    {
        Contract.Requires<ArgumentNullException>(function != null);

        TaskCompletionSource<TResult> taskCompletionSource = new TaskCompletionSource<TResult>();
        try
        {
            //没有设置同步上下文,直接在当前线程调用方法
            if (synchronizationContext == null)
            {
                TResult result = function.InvokeWith(executionContext);
                taskCompletionSource.SetResult(result);
            }
            else
            {
                // See: System.Runtime.CompilerServices.AsyncVoidMethodBuilder.Create()
                synchronizationContext.OperationStarted();
                // See: System.Threading.Tasks.SynchronizationContextAwaitTaskContinuation.PostAction()
                synchronizationContext.Post(_ =>
                {
                    try
                    {
                        TResult result = function.InvokeWith(executionContext);
                        // See: System.Runtime.CompilerServices.AsyncVoidMethodBuilder.NotifySynchronizationContextOfCompletion()
                        synchronizationContext.OperationCompleted();
                        taskCompletionSource.SetResult(result);
                    }
                    catch (Exception exception)
                    {
                        taskCompletionSource.SetException(exception);
                    }
                }, null);
            }
        }
        catch (Exception exception)
        {
            taskCompletionSource.SetException(exception);
        }

        return taskCompletionSource.Task;
    }
}

action版本:

public static class ActionExtensions
{
    public static Task InvokeWith(this Action action, SynchronizationContext synchronizationContext, ExecutionContext executionContext)
    {
        Contract.Requires<ArgumentNullException>(action != null);

        return new Func<object>(() =>
        {
            action();
            return null;
        }).InvokeWith(synchronizationContext, executionContext);
    }
}

ExecutionContext和SynchronizationContext的回调

通过上述扩展方法,可以为Task.ContinueWith()回调机制创建一些增强方法。这里它被称为ContinueWithContext(),因为它负责ContinueWith()的ExecutionContext和SynchronizationContext。该版本是为了continue一个方法:

public static class TaskExtensions
{
    public static Task<TNewResult> ContinueWithContext<TResult, TNewResult>(this Task<TResult> task, Func<Task<TResult>, TNewResult> continuation)
    {
        Contract.Requires<ArgumentNullException>(task != null);
        Contract.Requires<ArgumentNullException>(continuation != null);

        // See: System.Runtime.CompilerServices.AsyncMethodBuilderCore.GetCompletionAction()
        ExecutionContext executionContext = ExecutionContext.Capture();
        // See: System.Runtime.CompilerServices.AsyncVoidMethodBuilder.Create()
        // See: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run()
        SynchronizationContext synchronizationContext = SynchronizationContext.Current;
        return task.ContinueWith(t =>
                new Func<TNewResult>(() => continuation(t)).InvokeWith(synchronizationContext, executionContext))
            .Unwrap();
    }

    public static Task<TNewResult> ContinueWithContext<TNewResult>(this Task task, Func<Task, TNewResult> continuation)
    {
        Contract.Requires<ArgumentNullException>(task != null);
        Contract.Requires<ArgumentNullException>(continuation != null);

        // See: System.Runtime.CompilerServices.AsyncMethodBuilderCore.GetCompletionAction()
        ExecutionContext executionContext = ExecutionContext.Capture();
        // See: System.Runtime.CompilerServices.AsyncVoidMethodBuilder.Create()
        // See: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run()
        SynchronizationContext synchronizationContext = SynchronizationContext.Current;
        return task.ContinueWith(t => 
                new Func<TNewResult>(() => continuation(t)).InvokeWith(synchronizationContext, executionContext))
            .Unwrap();
    }
}

continue action:

public static class TaskExtensions
{
    public static Task ContinueWithContext<TResult>(this Task<TResult> task, Action<Task<TResult>> continuation)
    {
        Contract.Requires<ArgumentNullException>(task != null);
        Contract.Requires<ArgumentNullException>(continuation != null);

        return task.ContinueWithContext(new Func<Task<TResult>, object>(t =>
        {
            continuation(t);
            return null;
        }));
    }

    public static Task ContinueWithContext(this Task task, Action<Task> continuation)
    {
        Contract.Requires<ArgumentNullException>(task != null);
        Contract.Requires<ArgumentNullException>(continuation != null);

        return task.ContinueWithContext(new Func<Task, object>(t =>
        {
            continuation(t);
            return null;
        }));
    }
}

上面的WPF代码可以简单地修复:

this.Button.Click += (sender, e) =>
{
    // string html = await new WebClient().DownloadStringTaskAsync("https://weblogs.asp.net/dixin");
    new WebClient().DownloadStringTaskAsync("https://weblogs.asp.net/dixin").ContinueWithContext(await => { string html = await.Result;
    this.TextBox.Text = html; });
};

使用Task.ConfigureAwait()

Task.ConfigureAwait()是.NET提供的又一个有趣的API:

  • 当调用 Task.ConfigureAwait(continueOnCapturedContext: true) 时,初始 ExecutionContext 和 SynchronizationContext 都将被捕获以用于延续代码,这是上面解释的默认行为。
  • 调用 Task.ConfigureAwait(continueOnCapturedContext: false) 时,只会为延续代码捕获初始 ExecutionContext,而不是初始 SynchronizationContext。

for example,在上面的WPF应用中:

this.Button.Click += async (sender, e) =>
{
    await Task.Run(() => { }).ConfigureAwait(false);
    this.TextBox.Text = string.Empty; // Will not work.
};

这会和上面的Task.ContinueWith()一样抛出InvalidOperationException:

The calling thread cannot access this object because a different thread owns it.

总结

  1. 在编译器中:

    • 编译器决定一个对象是可等待的,如果:
      • 它有一个 GetAwaiter() 方法(实例方法或扩展方法);
      • 它的 GetAwaiter() 方法返回一个awaiter。如果满足以下条件,编译器会判定对象是awaiter:
        • 它实现了 INotifyCompletion 或 ICriticalNotifyCompletion 接口;
        • 它有一个 IsCompleted 属性,它有一个 getter 方法并返回一个Boolean;
        • 它有一个 GetResult() 方法,该方法返回 void(对应Task) 或 TResult(对应Task)。
  2. 编译中:

    • async修饰符被去到了;
    • await关键字也被去掉了,整个异步方法被被编译成带有 MoveNext() 方法的状态机
    • MoveNext() 方法可以以回调方式多次调用,
  3. 运行时:

    • await的初始 ExecutionContext 始终被捕获,并且其后续代码被编组到此捕获的 ExecutionContext。
    • 默认情况下会捕获await的初始 SynchronizationContext,并且其延续代码将编组到此捕获的 SynchronizationContext,除非像调用 Task.ConfigureAwait(false) 那样显式抑制。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值