c#:深入理解SynchronizationContext

环境:

  • window10
  • vs2019
  • .net core 3.1
  • dnSpy v6.1.4 (.NET Core)
  • ILSpy版本6.1.0.5902

参考:《C# SynchronizationContext线程上下文简单说明》

顺便说下: 单独看SynchronizationContext意义不大,因为它是一个基类,在真正使用它的时候一般都要继承并覆盖其方法的(如:winform)。

一、为什么需要SynchronizationContext?

问题是这样的:在winform中窗体的控件都是由UI线程创建的,当我们点击按钮后如果有耗时操作的话,我们肯定需要单独启动线程去运算,运算完了之后又要访问窗体控件将结果显示出来,但因为窗体控件是由UI线程创建的,所以,winform仅允许我们在UI线程中去操纵窗体控件。

所以就有了下面的常见代码:

private void button1_Click(object sender, EventArgs e)
{
    button1.Text = "运算中...";
    button1.Enabled = false;
    Task.Run(() =>
    {
        //耗时操作
        Thread.Sleep(3000);
        //运算完成
        button1.BeginInvoke((MethodInvoker)delegate
        {
            button1.Text = "点击运算";
            button1.Enabled = true;
        });
    });
}

现在尝试换一种写法,使用SynchronizationContext可以达到相同的效果:

private void button1_Click(object sender, EventArgs e)
{
    button1.Text = "运算中...";
    button1.Enabled = false;
    var ctx = SynchronizationContext.Current;
    Task.Run(() =>
    {
        //耗时操作
        Thread.Sleep(3000);
        //运算完成
        ctx.Send(state =>
        {
            button1.Text = "点击运算";
            button1.Enabled = true;
        }, null);
    });
}

从上面看,SynchronizationContext好像是将代码变得简洁了一些。当时微软推出SynchronizationContext的时候是想做一个多线程协同工作的标准(参考:《并行计算 SynchronizationContext 综述》)。

二、SynchronizationContext的特点

  • 首先,这是一个基类,它只提供了基本的实现,如果要使用它大多都需要继承并覆盖其中的方法;
  • 其次,它提供了一个在多个线程间同步工作的标准(比如:线程A需要线程B协助做一些事情等等)。

三、在控制台中自实现一个多线程同步模型

为了更好的理解SynchronizationContext,我们在控制台实现一个多线程同步模型,目的是让两个线程在执行的时候相互协作。整个过程模拟的是Winform事件处理代码,即上面的示例。

  • 第一步,因为SynchronizationContext本身只做了简单的定义,不满足我们的需要,所以定义一个MySynchronizationContext(上面示例代码中的SynchronizationContext.Current也不是SynchronizationContext类型,而是WindowsFormsSynchronizationContext):
  • public class MySynchronizationContext : SynchronizationContext
    {
        private readonly MyControl ctrl;
    
        public MySynchronizationContext(MyControl ctrl)
        {
            this.ctrl = ctrl;
        }
        //让主线程(UI线程)同步执行d方法
        public override void Send(SendOrPostCallback d, object state)
        {
            ctrl.Invoke(state => d(state), state);
        }
    	//让主线程(UI线程)异步执行d方法
        public override void Post(SendOrPostCallback d, object state)
        {
            ctrl.BeginInvoke(state => d(state), state);
        }
    }
    
  • 第二步,为了模拟窗体控件,我们自定义一个MyControl类,它在主线程(UI线程)中被创建,对它的修改也要在主线程中进行,代码如下:
    public class MyControl
    {
        //记录创建这个控件的线程(UI线程)
        private Thread _createThread = null;
        public MyControl()
        {
            //第一个控件创建时初始化一个SynchronizationContext实例,并将它和当前线程绑定一起
            if (SynchronizationContext.Current == null) SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext(this));
            _createThread = Thread.CurrentThread;
            //初始化一个字典队列,key: 线程Id,value:参数队列
            ExeArg.QueueDics.TryAdd(Thread.CurrentThread.ManagedThreadId, new BlockingCollection<ExeArg>());
        }
        //同步调用
        public void Invoke(Action<object> action, object state)
        {
            var queues = ExeArg.QueueDics[_createThread.ManagedThreadId];
            ManualResetEvent manualResetEvent = new ManualResetEvent(false);
            queues.Add(new ExeArg()
            {
                Action = obj =>
                {
                    action(state);
                    manualResetEvent.Set();
                },
                State = state,
                Sync = true
            });
            manualResetEvent.WaitOne();
            manualResetEvent.Dispose();
        }
        //异步调用
        public void BeginInvoke(Action<object> action, object state)
        {
            var queues = ExeArg.QueueDics[_createThread.ManagedThreadId];
            queues.Add(new ExeArg()
            {
                Action = action,
                State = state
            });
        }
    }
    
  • 第三步,为了封装委托和参数,我们需要定义一个模型:
    public class ExeArg
    {
        //要放到主线程(UI线程)中执行的方法
        public Action<object> Action { get; set; }
        //方法执行时额外的参数
        public object State { get; set; }
        //是否同步执行
        public bool Sync { get; set; }
        //静态字典,key: 线程Id,value: 队列
        public static ConcurrentDictionary<int, BlockingCollection<ExeArg>> QueueDics = new ConcurrentDictionary<int, BlockingCollection<ExeArg>>();
        //当前线程对应的字典
        public static BlockingCollection<ExeArg> CurrentQueue => QueueDics.ContainsKey(Thread.CurrentThread.ManagedThreadId) ? QueueDics[Thread.CurrentThread.ManagedThreadId] : null;
    }
    
  • 第四步,现在我们开始模拟整个流程
    public static void Main(string[] arg)
    {
        Console.WriteLine($"主线程: {Thread.CurrentThread.ManagedThreadId}");
        //主线程创建控件
        var ctrl = new MyControl();
        var syncContext = SynchronizationContext.Current;
        //模拟一个用户操作
        Task.Run(() =>
        {
        	//模拟耗时操作
            Thread.Sleep(2000);
            Console.WriteLine($"用户线程: {Thread.CurrentThread.ManagedThreadId},Post前");
            syncContext.Post((state) =>
            {
            	//可以在这里修改MyControl的值等
                Console.WriteLine($"Post内的方法执行线程: {Thread.CurrentThread.ManagedThreadId},参数:{state}");
            }, new { name = "小明" });
            Console.WriteLine($"用户线程: {Thread.CurrentThread.ManagedThreadId},Post后,Send前");
            syncContext.Send((state) =>
            {
            	//可以在这里修改MyControl的值等
            	//为了体现Send的同步效果,这里睡3秒
                Thread.Sleep(3000);
                Console.WriteLine($"Send内的方法执行线程: {Thread.CurrentThread.ManagedThreadId},参数:{state}");
            }, new { name = "小红" });
            Console.WriteLine($"用户线程: {Thread.CurrentThread.ManagedThreadId},Send后");
        });
        //主线程开启消息垒
        while (true)
        {
            var exeArg = ExeArg.CurrentQueue.Take();
            exeArg.Action?.Invoke(exeArg.State);
        }
    }
    

将程序运行起来,我们观察下结果:
c#:深入理解SynchronizationContext_第1张图片

从运行的结果上来看,我们达到了类Winform的多线程协作的目的。

四、Winform中怎样使用SynchronizationContext的?

其实,上面写的控制台示例程序就是本人在看过winform的相关代码后写出来的,它们逻辑大体相同。

  • 首先,Winform中用的是WindowsFormsSynchronizationContext,它继承自SynchronizationContext并重写了SendPost方法,在重写的时候调用了控件的InvokeBeginInvoke方法,如下:
    c#:深入理解SynchronizationContext_第2张图片
  • 然后,SynchronizationContext.Current的值是在第一个Control控件创建的时候被初始化的,如下图:
    c#:深入理解SynchronizationContext_第3张图片
    c#:深入理解SynchronizationContext_第4张图片

c#:深入理解SynchronizationContext_第5张图片
c#:深入理解SynchronizationContext_第6张图片

五、附控制台示例完整代码

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlTypes;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;

namespace Test
{
    class Program
    {
        public static void Main(string[] arg)
        {
            Console.WriteLine($"主线程: {Thread.CurrentThread.ManagedThreadId}");
            //主线程创建控件
            var ctrl = new MyControl();
            var syncContext = SynchronizationContext.Current;
            //模拟一个用户操作
            Task.Run(() =>
            {
                Thread.Sleep(2000);
                Console.WriteLine($"用户线程: {Thread.CurrentThread.ManagedThreadId},Post前");
                syncContext.Post((state) =>
                {
                    Console.WriteLine($"Post内的方法执行线程: {Thread.CurrentThread.ManagedThreadId},参数:{state}");
                }, new { name = "小明" });
                Console.WriteLine($"用户线程: {Thread.CurrentThread.ManagedThreadId},Post后,Send前");
                syncContext.Send((state) =>
                {
                    Thread.Sleep(3000);
                    Console.WriteLine($"Send内的方法执行线程: {Thread.CurrentThread.ManagedThreadId},参数:{state}");
                }, new { name = "小红" });
                Console.WriteLine($"用户线程: {Thread.CurrentThread.ManagedThreadId},Send后");
            });
            //主线程开启消息垒
            while (true)
            {
                var exeArg = ExeArg.CurrentQueue.Take();
                exeArg.Action?.Invoke(exeArg.State);
            }
        }
    }

    public class ExeArg
    {
        //要放到主线程(UI线程)中执行的方法
        public Action<object> Action { get; set; }
        //方法执行时额外的参数
        public object State { get; set; }
        //是否同步执行
        public bool Sync { get; set; }
        //静态字典,key: 线程Id,value: 队列
        public static ConcurrentDictionary<int, BlockingCollection<ExeArg>> QueueDics = new ConcurrentDictionary<int, BlockingCollection<ExeArg>>();
        //当前线程对应的字典
        public static BlockingCollection<ExeArg> CurrentQueue => QueueDics.ContainsKey(Thread.CurrentThread.ManagedThreadId) ? QueueDics[Thread.CurrentThread.ManagedThreadId] : null;
    }

    public class MySynchronizationContext : SynchronizationContext
    {
        private readonly MyControl ctrl;

        public MySynchronizationContext(MyControl ctrl)
        {
            this.ctrl = ctrl;
        }
        public override void Send(SendOrPostCallback d, object state)
        {
            ctrl.Invoke(state => d(state), state);
        }

        public override void Post(SendOrPostCallback d, object state)
        {
            ctrl.BeginInvoke(state => d(state), state);
        }
    }

    public class MyControl
    {
        //记录创建这个控件的线程(UI线程)
        private Thread _createThread = null;
        public MyControl()
        {
            //第一个控件创建时初始化一个SynchronizationContext实例,并将它和当前线程绑定一起
            if (SynchronizationContext.Current == null) SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext(this));
            _createThread = Thread.CurrentThread;
            //初始化一个字典队列,key: 线程Id,value:参数队列
            ExeArg.QueueDics.TryAdd(Thread.CurrentThread.ManagedThreadId, new BlockingCollection<ExeArg>());
        }
        //同步调用
        public void Invoke(Action<object> action, object state)
        {
            var queues = ExeArg.QueueDics[_createThread.ManagedThreadId];
            ManualResetEvent manualResetEvent = new ManualResetEvent(false);
            queues.Add(new ExeArg()
            {
                Action = obj =>
                {
                    action(state);
                    manualResetEvent.Set();
                },
                State = state,
                Sync = true
            });
            manualResetEvent.WaitOne();
            manualResetEvent.Dispose();
        }
        //异步调用
        public void BeginInvoke(Action<object> action, object state)
        {
            var queues = ExeArg.QueueDics[_createThread.ManagedThreadId];
            queues.Add(new ExeArg()
            {
                Action = action,
                State = state
            });
        }
    }
}

你可能感兴趣的:(.netcore,c#)