从执行上下文角度重新理解.NET(Core)的多线程编程[2]:同步上下文
一般情况下,我们可以将某项操作分发给任意线程来执行,但有的操作确实对于执行的线程是有要求的,最为典型的场景就是:GUI针对UI元素的操作必须在UI主线程中执行。将指定的操作分发给指定线程进行执行的需求可以通过同步上下文(SynchronizationContext)来实现。你可能从来没有使用过SynchronizationContext,但是在基于Task的异步编程中,它却总是默默存在。今天我们就来认识一下这个SynchronizationContext对象。
目录
一、从一个GUI的例子谈起
二、自定义一个SynchronizationContext
三、ConfiguredTaskAwaitable方法
四、再次回到开篇的例子
一、从一个GUI的例子谈起
GUI后台线程将UI操作分发给UI主线程进行执行时SynchronizationContext的一个非常典型的应用场景。以一个Windows Forms应用为例,我们按照如下的代码注册了窗体Form1的Load事件,事件处理器负责修改当前窗体的Text属性。由于我们使用了线程池,所以针对UI元素的操作(设置窗体的Text属性)将不会再UI主线程中执行。
partial class Form1
{
private void InitializeComponent()
{
...
this.Load += Form1_Load;
}
private void Form1_Load(object sender, EventArgs e)=>ThreadPool.QueueUserWorkItem(_ => Text = "Hello World");
}
当这个Windows Forms应用启动之后,设置Form1的Text属性的那行代码将会抛出如下所示的InvalidOperationException异常,并提示“Cross-thread operation not valid: Control '' accessed from a thread other than the thread it was created on.”
我们可以按照如下的方式利用SynchronizationContext来解决这个问题。如代码片段所示,在利用线程池执行异步操作之前,我们调用Current静态属性得到当前的SynchronizationContext。对于GUI应用来说,这个同步上下文将于UI线程绑定在一起,我们可以利用它将指定的操作分发给UI线程来执行。具体来说,针对UI线程的分发是通过调用其Post方法来完成的。
partial class Form1
{
private void InitializeComponent()
{
...
this.Load += Form1_Load;
}
private void Form1_Load(object sender, EventArgs e)
{
var syncContext = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(_ => syncContext.Post(_=>Text = "Hello World", null));
}
}
二、自定义一个SynchronizationContext
虽然被命名为SynchronizationContext,并且很多场景下我们利用该对象旨在异步线程中同步执行部分操作的问题(比如上面这个例子),但原则上可以利用自定义的SynchronizationContext对分发给的操作进行100%的控制。在如下的代码中,我们创建一个FixedThreadSynchronizationContext类型,它会使用一个单一固定的线程来执行分发给它的操作。FixedThreadSynchronizationContext继承自SynchronizationContext,它将分发给它的操作(体现为一个SendOrPostCallback类型的委托)置于一个队列中,并创建一个独立的线程依次提取它们并执行。
public class FixedThreadSynchronizationContext:SynchronizationContext
{
private readonly ConcurrentQueue<(SendOrPostCallback Callback, object State)> _workItems;
public FixedThreadSynchronizationContext()
{
_workItems = new ConcurrentQueue<(SendOrPostCallback Callback, object State)>();
var thread = new Thread(StartLoop);
Console.WriteLine("FixedThreadSynchronizationContext.ThreadId:{0}", thread.ManagedThreadId);
thread.Start();
void StartLoop()
{
while (true)
{
if (_workItems.TryDequeue(out var workItem))
{
workItem.Callback(workItem.State);
}
}
}
}
public override void Post(SendOrPostCallback d, object state) => _workItems.Enqueue((d, state));
public override void Send(SendOrPostCallback d, object state)=> throw new NotImplementedException();
}
向SynchronizationContext分发指定的操作可以调用Post和Send方法,它们之间差异就是异步和同步的差异。FixedThreadSynchronizationContext仅仅重写了Post方法,意味着它支持异步分发,而不支持同步分发。我们采用如下的方式来使用FixedThreadSynchronizationContext。我们先创建一个FixedThreadSynchronizationContext对象,并采用线程池的方式同时执行5个异步操作。对于我们异步操作来说,我们先调用静态方法SetSynchronizationContext将创建的这个FixedThreadSynchronizationContext对象设置为当前SynchronizationContext。然后调用Post方法将指定的操作分发给当前SynchronizationContext。置于具体的操作,它会打印出当前线程池线程和当前操作执行线程的ID。
class Program
{
static async Task Main()
{
var synchronizationContext = new FixedThreadSynchronizationContext();
for (int i = 0; i < 5; i++)
{
ThreadPool.QueueUserWorkItem(_ =>
{
SynchronizationContext.SetSynchronizationContext(synchronizationContext);
Invoke();
});
}
Console.Read();
void Invoke()
{
var dispatchThreadId = Thread.CurrentThread.ManagedThreadId;
SendOrPostCallback callback = _ => Console.WriteLine($"Pooled Thread: {dispatchThreadId}; Execution Thread: {Thread.CurrentThread.ManagedThreadId}");
SynchronizationContext.Current.Post(callback, null);
}
}
}
这段演示程序执行之后会输出如下所示的结果,可以看出从5个线程池线程分发的5个操作均是在FixedThreadSynchronizationContext绑定的那个线程中执行的。
三、ConfiguredTaskAwaitable方法
我知道很少人会显式地使用SynchronizationContext上下文,但是正如我前面所说,在基于Task的异步编程中,SynchronizationContext上下文其实一直在发生作用。我们可以通过如下这个简单的例子来证明SynchronizationContext的存在。如代码片段所示,我们创建了一个FixedThreadSynchronizationContext对象并通过调用SetSynchronizationContext方法将其设置为当前SynchronizationContext。在调用Task.Delay方法(使用await关键字)等待100ms之后,我们打印出当前的线程ID。
class Program
{
static async Task Main()
{
SynchronizationContext.SetSynchronizationContext(new FixedThreadSynchronizationContext());
await Task.Delay(100);
Console.WriteLine("Await Thread: {0}", Thread.CurrentThread.ManagedThreadId);
}
}
如下所示的是程序运行之后的输出结,可以看出在await Task之后的操作实际是在FixedThreadSynchronizationContext绑定的那个线程上执行的。在默认情况下,Task的调度室通过ThreadPoolTaskScheduler来完成的。顾名思义,ThreadPoolTaskScheduler会将Task体现的操作分发给线程池中可用线程来执行。但是当它在分发之前会先获取当前SynchronizationContext,并将await之后的操作分发给这个同步上下文来执行。
如果不了解这个隐含的机制,我们编写的异步程序可能会导致很大的性能问题。如果多一个线程均将这个FixedThreadSynchronizationContext作为当前SynchronizationContext,意味着await Task之后的操作都将分发给一个单一线程进行同步执行,但是这往往不是我们的真实意图。其实这个问题很好解决,我们只需要调用等待Task的ConfiguredTaskAwaitable方法,并将参数设置为false显式指示后续的操作无需再当前SynchronizationContext中执行。
class Program
{
static async Task Main()
{
SynchronizationContext.SetSynchronizationContext(new FixedThreadSynchronizationContext());
await Task.Delay(100).ConfigureAwait(false);
Console.WriteLine("Await Thread: {0}", Thread.CurrentThread.ManagedThreadId);
}
}
再次执行该程序可以从输出结果看出await Task之后的操作将不会自动分发给当前的FixedThreadSynchronizationContext了。
四、再次回到开篇的例子
由于SynchronizationContext的存在,所以如果将开篇的例子修改成如下的形式是OK的,因为await之后的操作会通过SynchronizationContext分发到UI主线程执行。
partial class Form1
{
private void InitializeComponent()
{
...
this.Load += Form1_Load;
}
private async void Form1_Load(object sender, EventArgs e)
{
await Task.Delay(1000);
Text = "Hello World";
}
}
但是如果添加了ConfigureAwait(false)方法的调用,依然会抛出上面遇到的InvalidOperationException异常。
partial class Form1
{
private void InitializeComponent()
{
...
this.Load += Form1_Load;
}
private async void Form1_Load(object sender, EventArgs e)
{
await Task.Delay(1000).ConfigureAwait(false);
Text = "Hello World";
}
}
从执行上下文角度重新理解.NET(Core)的多线程编程[1]:基于调用链的”参数”传递
从执行上下文角度重新理解.NET(Core)的多线程编程[2]:同步上下文
从执行上下文角度重新理解.NET(Core)的多线程编程[3]:安全上下文
从执行上下文角度重新理解.NET(Core)的多线程编程[2]:同步上下文的更多相关文章
- 从执行上下文角度重新理解.NET(Core)的多线程编程[1]:基于调用链的”参数”传递
线程是操作系统能够进行运算调度的最小单位,操作系统线程进一步被封装成托管的Thread对象,手工创建并管理Thread对象已经成为了所能做到的对线程最细粒度的控制了.后来我们有了ThreadPool, ...
- 执行上下文与同步上下文 | ExecutionContext 和 SynchronizationContext
原文连接:执行上下文与同步上下文 - .NET 并行编程 (microsoft.com) 执行上下文与同步上下文 斯蒂芬 6月15日, 2012 最近,我被问了几次关于 ExecutionContex ...
- SynchronizationContext(同步上下文)综述
>>返回<C# 并发编程> 1. 概述 2. 同步上下文 的必要性 2.1. ISynchronizeInvoke 的诞生 2.2. SynchronizationContex ...
- [翻译 EF Core in Action 2.3] 理解EF Core数据库查询
Entity Framework Core in Action Entityframework Core in action是 Jon P smith 所著的关于Entityframework Cor ...
- 【C# Task】理解Task中的ConfigureAwait配置同步上下文
原文:https://devblogs.microsoft.com/dotnet/configureawait-faq/ 作者:Stephen 翻译:xiaoxiaotank 静下心来,你一定会有收获 ...
- Android AsyncTask完全解析,带你从源码的角度彻底理解
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/11711405 我们都知道,Android UI是线程不安全的,如果想要在子线程里进 ...
- 从逆向的角度去理解C++虚函数表
很久没有写过文章了,自己一直是做C/C++开发的,我一直认为,作为一个C/C++程序员,如果能够好好学一下汇编和逆向分析,那么对于我们去理解C/C++将会有很大的帮助,因为程序中所有的奥秘都藏在汇编中 ...
- [转]Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
Android事件分发机制 该篇文章出处:http://blog.csdn.net/guolin_blog/article/details/9097463 其实我一直准备写一篇关于Android事件分 ...
- 从源码角度深入理解Handler
为了获得良好的用户体验,Android不允许开发者在UI线程中调用耗时操作,否则会报ANR异常,很多时候,比如我们要去网络请求数据,或者遍历本地文件夹都需要我们在新线程中来完成,新线程中不能更新UI, ...
随机推荐
- Hive sql函数
date: 2018-11-16 19:03:08 updated: 2018-11-16 19:03:08 Hive sql函数 一.关系运算 等值比较: = select 1 from dual ...
- zookeeper Cli的常用命令
zookeeper Cli的常用命令 服务管理 启动ZK服务: zkServer.sh start 查看ZK状态: zkServer.sh status 停止ZK服务: zkServer.sh sto ...
- Spring源码分析之`BeanFactoryPostProcessor`调用过程
前文传送门: Spring源码分析之预启动流程 Spring源码分析之BeanFactory体系结构 本文内容: AbstractApplicationContext#refresh前部分的一点小内容 ...
- 手撸ORM浅谈ORM框架之Delete篇
快速传送 手撸ORM浅谈ORM框架之基础篇 手撸ORM浅谈ORM框架之Add篇 手撸ORM浅谈ORM框架之Update篇 手撸ORM浅谈ORM框架之Delete篇 手撸ORM浅谈ORM框架之Query ...
- typeerror object of type ‘decimal‘ is not json serializable jsonify
当使用flask的jsonify返回json数据时,由于数据库有些字段类型使用decimal,而jsonify无法处理 解决方案 导入下面的包即可解决 pip install simplejson
- Bucardo使用文档-lottu
官网地址 一.Bucardo介绍 Bucardo 是基于表复制的系统 Bucardo 可以实现PostgreSQL数据库的双master/多master的方案 Bucardo的核心是一个Perl守护进 ...
- 《Clojure编程》笔记 第13章 测试
目录 背景简述 第13章 测试 13.1 术语 13.2 clojure.test 13.2.1 定义测试的两种方式 13.2.1.1 用deftest宏把测试定义成单独的函数 13.2.1.2 用w ...
- QQ 邮箱日历提醒
偶然发现 QQ 邮箱有日历的功能,而且可以设置农历并且每年邮件 + 短信 + 微信提醒.这下重要的日子(eg:生日...)就不会忘记啦! 1.找到日历 2.历史提醒 3.新建时间 4.设置时间 5.勾 ...
- 在嵌入式设备中实现webrtc的第三种方式③
本系列的最后一篇,讲解收发音视频数据. 贴出最终效果: 其实很简单,直接调用writeFrame即可,如下图: 当然,这是部分代码,完整代码在下面,展开可见: 1 #include "com ...
- n阶行列式的全排列求解(Java)
上一个随笔,我介绍了全排列的递归求解,其中还有排列的逆序数等代码,这次我来介绍如何使用全排列计算行列式的值. 使用全排列求行列式的值,简单的描述就是: 对这个行列式每一行选取一个数,这些数处于行列式的 ...