C# 同步上下文及死锁
1,同步上下文的概念及其历史
在 .Net 之前,多线程的应用程序就已经存在了,这些程序经常需要把比如当前线程的 工作状态或者上下文传递到另一个线程中,在 windows中,程序都是以一个总的消息循环中心来分发所有信息的。所以最开始的时候,大家都会通过定义自己的windows消息和处理他的约定来进行处理。
我们都知道的是 Windows 上的程序是以 消息循环为中心的,这个如何理解呢?
每一个 window 窗体都有一个与之关联的 Window Procedure,这个是一个用来处理所有消息发送或发送到类的给所有消息的函数。窗体的所有UI显示和显示都取决于 Window Procedure 对这些消息的响应。
当 .Net Framework 首次发布时,当时唯一的一个 GUI应用就是 Windows Form。他们的框架设计者开发了一个通用的解决方案 -> ISynchronizeInvoke 诞生啦。
当时框架设计者设计 ISynchroizeInvoke 背后的想法是这样的:源线程 可以对 目标线程的委托 进行排队,可以选择等待该委托完成。并且 ISynchronizeInvoke 还提供了一个属性来确定当前代码是否已经在 目标线程上运行,在这种情况下,就没有必要排队等待委托了。
然而当 .Net Framework 2.0 发布后,发现这种模式又不适用了,主要是因为创建Web页面通常依赖于数据库查询和对Web服务的调用,处理该请求的线程必须等待,直到这些操作完成。
后面,框架设计者设计出了 SynchronizationContext,这就是我们呢到现如今,.Net Framework4.8 .NET 5 还在使用的同步上下文 模型
2,如何理解同步上下文
Provides the basic functionality for propagating a synchronization context in various synchronization models. 提供在各种同步模型中传播 同步上下文的功能。
1,它提供了一种将工作单元排队到上下文的方法。请注意,此工作单元是排队到一个上下文,而不是特定的线程。这个区别很重要,因为许多SynchronizationContext的实现不是基于单个的、特定的线程。
2,每个线程都有一个当前上下文。一个线程的上下文不一定是唯一的;它的上下文实例可以与其他线程共享。一个线程可以改变它的当前上下文,但一般很少。
3,它保存了未完成异步操作的计数。这使得使用ASP.NET异步页面和任何其他需要这种计数的主机。在大多数情况下,当捕获当前的SynchronizationContext时,该计数将增加,而当捕获的SynchronizationContext用于将完成通知排队发送到该上下文时,该计数将减少。
WindowsFormsSynchronizationContext (System.Windows.Forms.dll)
1,Use ISynchronizeInvoke on UI Control,用来将委托传递 给 win32 message loop
2,每一个 UI 线程 都会创建一个 WindowsFormsSynchronizationContext
3,WindowsFormsSynchronizationContext 的上下文是一个 单一的UI 线程
DispatcherSynchronizationContext(WindowsBase.dll: System.Windows.Threading)
1,以 “Normal” 优先级的委托 传递给 UI 线程。
2,所有排队到 DispatcherSynchronizationContext 的委托都是由 特定的UI线程 按照他们排队的顺序 依次执行,一次执行一个。
3,DispatcherSynchronizationContext 的上下文是一个 单一的 UI 线程
Default(ThreadPool) SynchronizationContext(mscorlib.dll: System.Threading)
1,默认的 SynchronizationContext 是一个默认构造函数的 SynchronizationContext 对象,按照惯例,如果一个线程 当前的 SynchronizationContext 是 null,那么它会 隐式的 含有一个 默认的 SynchronizationContext。
2,默认 SynchronizationContext 将它的异步委托 添加到 线程池 队列,但是 在调用的线程上执行它的同步委托。因此,它的上下文 涵盖了 所有的线程池的线程 以及 调用它的线程。上下文 “借用” 调用它的线程,然后把它们带入到上下文中 直到委托结束。从某种意义上来说,默认上下文可能包含当前进程中的任何线程。
3,默认 SynchronizationContext 是应用在 线程池中的线程的除非是被 ASP.NET 托管的代码,默认的 SynchronizationContext 也隐式应用于显式的子线程中除非子线程设置了自己的 SynchronizationContext。因此,UI 的应用一般都有两个 SynchronizationContext
, UI 的 SynchronizationContext cover UI thread, default SynchronizationContext
cover ThreadPool thread。
考虑这样一个场景:
一个窗体,上面有一个按钮,点击时从 www.baidu.com上获取一些内容。
这个Button UI 线程唯一拥有,并可以访问它,否则的话就会出现我们经常看到的一个错误
System.InvalidOperationException: ‘The calling thread cannot access this object because a different thread owns it.‘
用 同步上下文来实现的话,就可以用这样。显式的声明回调后的函数处理。
然而,这种方式可能才是我们最经常用的。当我们使用 “await”时,做的事情呢其实和我们 主动调用 callback 进行回调处理是一样的。接下来我们深入底层来 剖析下 这个 await 具体都做了啥?
我们都知道 当我们给 一个方法 声明 async 时,这个方法会被C# 中的编译器转成 一个 StateMachine 状态机。
以下面这个简单的方法声明为例子:
private async Task Run()
{
await M();
}
[AsyncStateMachine(typeof(<run>d__1))]
[DebuggerStepThrough]
private Task Run()
{
<run>d__1 stateMachine = new <run>d__1();
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.<>4__this = this;
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
[CompilerGenerated]
private sealed class <run>d__1 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
public C <>4__this;
private TaskAwaiter <>u__1;
private void MoveNext()
{
int num = <>1__state;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = <>4__this.M().GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
<run>d__1 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
}
awaiter.GetResult();
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
3,说下 ConfigureAwait
4,说下 死锁
直接拿 async.cs 中的状态机来讲
重点在 SetResult
疑问?为什么用 await 不会死锁,而用 Wait() 或 GetAwaiter().GetResult() 就会死锁。
下面三张可以比较形象的
5,说下 async void
如非必要,尽量不用,除了 事件处理方法声明上。
开始很容易,结束太难。因为你根本不知道什么时候它执行结束了。
很难编写单元测试代码。