【深入浅出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.
总结
-
在编译器中:
- 编译器决定一个对象是可等待的,如果:
- 它有一个 GetAwaiter() 方法(实例方法或扩展方法);
- 它的 GetAwaiter() 方法返回一个awaiter。如果满足以下条件,编译器会判定对象是awaiter:
- 它实现了 INotifyCompletion 或 ICriticalNotifyCompletion 接口;
- 它有一个 IsCompleted 属性,它有一个 getter 方法并返回一个Boolean;
- 它有一个 GetResult() 方法,该方法返回 void(对应Task) 或 TResult(对应Task)。
- 编译器决定一个对象是可等待的,如果:
-
编译中:
- async修饰符被去到了;
- await关键字也被去掉了,整个异步方法被被编译成带有 MoveNext() 方法的状态机
- MoveNext() 方法可以以回调方式多次调用,
-
运行时:
- await的初始 ExecutionContext 始终被捕获,并且其后续代码被编组到此捕获的 ExecutionContext。
- 默认情况下会捕获await的初始 SynchronizationContext,并且其延续代码将编组到此捕获的 SynchronizationContext,除非像调用 Task.ConfigureAwait(false) 那样显式抑制。