C# 同步上下文及死锁

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

如非必要,尽量不用,除了 事件处理方法声明上。

开始很容易,结束太难。因为你根本不知道什么时候它执行结束了。

很难编写单元测试代码。

posted @ 2021-07-23 21:53  BUTTERAPPLE  阅读(657)  评论(0编辑  收藏  举报