专栏首页林德熙的博客C# dotnet 自己实现一个线程同步上下文

C# dotnet 自己实现一个线程同步上下文

昨天鹏飞哥问了我一个问题,为什么在控制台程序的主线程等待某个线程执行完成之后回来,是在其他线程执行的。而 WPF 在等待某个线程执行完成之后,可以回到主线程执行。其实这是因为在 WPF 和 WinForms 和 ASP.NET 框架里面都自己实现了线程同步上下文,通过线程同步上下文做到调度线程执行。本文就来和小伙伴聊一下如何自己实现一个线程同步上下文

我昨天和鹏飞哥说的时候感觉特别绕,但是实际上过来写了一点代码,又发现很好理解。其实线程同步上下文这个概念在于我能否返回到之前的线程,返回到之前的线程需要哪些内容。而 await 在出现线程切换的时候,是通过调用之前等待之前的线程的线程同步上下文进行线程调度,大概在进入 await 的做法如下

var currentSynchronizationContext = SynchronizationContext.Current;

// await 里面的复杂逻辑

   currentSynchronizationContext.Post(state =>
   {
      // 异步状态机调度过来的后面的任务
   }, state: null);

可以看到在 await 进入之前存放当前线程的同步上下文,而在执行完成之后,将后面的代码作为异步状态机调度创建委托,通过线程同步上下文的 Post 方法进行调度

那么什么是异步状态机调度过来的后面的任务,其实 await 只是语法加上很少的框架辅助做出来的,实际上代码就是通过一个个包装委托做的,如下面代码

await Task.Run(() => {});
Foo();

此时的代码按照同步上下文的调用,可以在 IL 里面做如下的翻译

// 在 await 任务之前先获取当前线程同步上下文
var currentSynchronizationContext = SynchronizationContext.Current;

Task.Run(() => {}).ContinueWith(t =>
{
      currentSynchronizationContext.Post(state =>
      {
          Foo();
	  }, state: null);
});

实际的 IL 会比上面代码复杂好多,原因是需要考虑存在多个不同的 await 以及不同的等待的内容的继续的写法,如 Task 通过调用 ContinueWith 方法在执行完成之后继续

从上面代码可以看到实际上线程同步上下文只是执行 await 后面的代码的方法,如果在调用 currentSynchronizationContext.Post 能让传入的委托在原有线程执行是不是就和 WPF 等框架相同

实际上 WPF 大概也是这样写的,下面来写一个自定义的线程同步上下文,让主线程加上线程同步上下文做到在等待其他线程执行完成返回可以到主线程执行

class SycnContext : SynchronizationContext

在继承了SynchronizationContext类,可以重写两个主要的方法,就是 Post 和 Send 方法。这两个方法的含义就是 Post 就是调用方不等待调用的内容执行完成,调用只是让他执行,不等待执行完成。而 Send 就是调用方需要等待 Send 传入的委托执行完成

        public override void Post(SendOrPostCallback d, object state)
        {
        }

        /// <inheritdoc />
        public override void Send(SendOrPostCallback d, object state)
        {
          
        }

这就是两个关键方法的重写,而默认的 SynchronizationContext 是如何实现的? 请看开源的 源代码 实际上十分简单

        public virtual void Send(SendOrPostCallback d, object? state) => d(state);

        public virtual void Post(SendOrPostCallback d, object? state) => ThreadPool.QueueUserWorkItem(s => s.d(s.state), (d, state), preferLocal: false);

可以看到默认的 Post 是通过线程池的方式调用,这就是为什么回不到主线程的原因

那么在重写这个方法如何让调用的内容回到主线程执行?回到主线程执行有前提是主线程需要有空,如果主线程没有空那么如何执行。从方法上传入的只是一个委托,如何让这个委托在主线程执行。这需要主线程主动去执行才可以

在 SycnContext 类添加一个锁,然后主线程空闲的时候就等待这个锁。而在有代码调用 Post 方法的时候,就释放这个锁,让主线程执行调用进来的委托

        public override void Post(SendOrPostCallback d, object state)
        {
            Run = () => d(state);
            Event.Set();
        }

        public Action Run { private set; get; }

        public AutoResetEvent Event { get; } = new AutoResetEvent(false);

上面代码的锁用的是 AutoResetEvent 类,这个类的功能就是在调用 WaitOne 的时候进入锁等待,直到其他线程调用了 Set 方法才会继续执行

在主线程可以等待 AutoResetEvent 如果等待返回了就执行 Run 委托

            var synchronizationContext = new SycnContext();

            while (true)
            {
                synchronizationContext.Event.WaitOne();
                synchronizationContext.Run();
            }

那么如何让 await 在执行之前可以拿到线程同步上下文?可以通过 SynchronizationContext 的一个静态方法设置线程静态字段

            var synchronizationContext = new SycnContext();
            
            SynchronizationContext.SetSynchronizationContext(synchronizationContext);

上面涉及到一个概念是线程静态字段,什么是线程静态字段,和静态字段有什么不同?在 dotnet 里面的静态字段是所有线程访问到的对象都是相同的对象。而线程静态字段是只有相同的线程才能访问到相同的对象,不同的线程访问到的是不同的对象。而上面代码是将线程同步上下文设置到当前的线程的一个线程静态字段里面,也就是在当前线程访问的线程同步上下文都是刚才设置的对象,但其他线程访问的是其他对象

请看官方的代码在获取当前线程同步上下文的代码

public static SynchronizationContext? Current => Thread.CurrentThread._synchronizationContext;

小伙伴都用过 Thread.CurrentThread 这个静态属性,这个属性返回的就是当前线程,也就是不同的线程拿到的对象是不同的。更多线程静态请看 dotnet 线程静态字段

现在添加一个等待后台线程的代码

        private static async void Foo()
        {
            var task = Task.Run(async () =>
            {
                await Task.Delay(100);
                Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
            });
            await task;
            Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
        }

在这个方法里面输出了当前的线程是哪个

现在的主函数如下

        static void Main(string[] args)
        {
            var synchronizationContext = new SycnContext();
            
            SynchronizationContext.SetSynchronizationContext(synchronizationContext);
         
            Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
            Foo();

            while (true)
            {
                synchronizationContext.Event.WaitOne();
                synchronizationContext.Run();
            }
        }

在运行的时候推荐添加断点,在 Foo(); 添加断点,在 while (true) 添加断点,在 await task; 添加断点,这样小伙伴就可以看到调用的顺序了

在调用 Foo() 方法进入到 await task; 方法的时候,主线程执行到 await 就出让执行权,返回到 Foo 外面,执行 while (true) 代码。在 Task 里面执行到了 await Task.Delay(100); 完成,再到输出当前线程是哪个之后,将会完成 await task; 的代码,此时将会通过 SynchronizationContext 的 Post 方法将后面的输出作为委托传入

在 Post 方法里面将会先设置 Run 委托,然后释放锁让主线程继续执行,主线程将会执行 Run 委托,也就是执行 await task; 之后的代码

因为是主线程执行 await task; 之后的代码,所以效果就是等待线程返回之后回到主线程继续执行

刚才的代码还少了 Send 方法,其实 Send 方法就是需要在执行完成传入的委托才能返回,可以通过一个锁来做

        public override void Send(SendOrPostCallback d, object state)
        {
            // 用于了解执行完成
            AutoResetEvent autoResetEvent = new AutoResetEvent(false);
            Run = () =>
            {
                d(state);
                autoResetEvent.Set();
            };
            Event.Set();
            autoResetEvent.WaitOne();
        }

那在 WPF 是如何实现的?其实 WPF 有一个 DispatcherSynchronizationContext 类,逻辑和上面自定义的差不多,请看源代码 代码核心通过 Dispatcher 实现

说起来也许复杂,但是写一写就知道是怎么弄的

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • asp dotnet core 记一次应用拒绝响应调试 开启线程等待同步用光线程池

    我有一个上古的库,我使用这个库用来上报日志,而刚才日志服务挂了。然后我就发现了我的应用拒绝响应了,通过 VisualStudio 断点调试可以发现线程池的线程全...

    林德熙
  • win10 uwp 线程池 为什么需要线程池什么是线程池线程池原理应用等待代码完成定时器

    如果大家有开发 WPF 或以前的程序,大概知道线程池不是 UWP 创造的,实际上在很多技术都用到线程池。 为什么需要线程池,他是什么?如何在 UWP 使用线程池...

    林德熙
  • dotnet 多线程禁止同时调用相同的方法 禁止方法重入调用 双检锁的设计

    大家在使用多线程的时候,是否有关注过线程安全的问题。如果咱的代码在使用多线程时,在相同的时间有多个线程同时执行相同的方法,此时也许就存在数据安全的问题,如多个线...

    林德熙
  • 【你问我答】这些Java并发问题,专家是这么回答的

    针对上期Java高并发【你问我答】中读者提出的问题,王锐同学的回答如下。 一 ---- 美团内部使用过Akka么?有什么坑? ——Absurd “ 答: 只简...

    美团技术团队
  • Vista 及后续版本的新线程池

    在上一篇的博文中,说了下老版本的线程池,在Vista之后,微软重新设计了一套线程池机制,并引入一组新的线程池API,新版线程池相对于老版本的来说,它的可控性更高...

    Masimaro
  • 第37节:多线程安全问题

    创建线程的方法 继承类Thread并重写run(),run()称为线程体;用这种方法定义的类不能再继承其他类。

    达达前端
  • Netty中的线程处理EventLoop

    运行任务处理的在编程上的构造通常称作事件循环,Netty使用EventLoop来描述。一个EventLoop将由一个永远不会变的Thread驱动,它可以被指派给...

    爬蜥
  • JMeter(连载3)

    这个组件用于测试流程的参数化,参数化文件采用类似于CSV文件。如图16所示。通过菜单“Add->Config Element->CSVData Set Conf...

    小老鼠
  • 让人头大的各种锁,从这里让你思绪清晰

    说到了锁我们经常会联想到生活中的锁,在我们日常中我们经常会接触到锁。比如我们的手机锁,电脑锁,再比如我们生活中的门锁,这些都是锁。

    乱敲代码
  • java高级应用:线程池全面解析

    什么是线程池? 很简单,简单看名字就知道是装有线程的池子,我们可以把要执行的多线程交给线程池来处理,和连接池的概念一样,通过维护一定数量的线程池来达到多个线程的...

    Java技术栈

扫码关注云+社区

领取腾讯云代金券