从执行上下文角度重新理解.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, ...
随机推荐
- postMessage 跨域
基于 postMessage 和 localStorage 的跨域本地存储方案 安·记 2014-09-07 2099 阅读 跨域 存储 localStorage HTML5 的 postMessag ...
- D. Concatenated Multiples 解析(思維)
Codeforce 1029 D. Concatenated Multiples 解析(思維) 今天我們來看看CF1029D 題目連結 題目 給你一個序列\(a\)和一個數字\(k\),求有幾種ind ...
- Java 复制到剪贴板
public void copy(String str) { StringSelection stsel = new StringSelection(str); Toolkit.getDefaultT ...
- 在国内使用Google验证码reCaptcha
如今各大网站都不可缺少的一部分就是验证码,验证码具有防止恶意批量操作,保护账户安全等作用.但是现在各种暴力破解验证码的手段层出不穷,验证码的保护也就失去了意义.所以各大平台为了应对这种情况也是使用类似 ...
- 走在深夜的小码农 Second Day
HTML5 Second Day writer:late at night codepeasant 学习大纲 表格 表格的主要作用 表格主要用于显示.展示数据,因为它可以让数据显示的非常的规整,可读性 ...
- Java学习的第十五天
1.今天复习了第四章的内容 重新看了看方法参数问题,final修饰的关键字 2.今天没问题 3.明天学习多态变化
- 04 . Go+Vue开发一个线上外卖应用(用户名密码和图形验证码)
图形化验证码生成和验证 功能介绍 在使用用户名和密码登录功能时,需要填写验证码,验证码是以图形化的方式进行获取和展示的. 验证码使用原理 验证码的使用流程和原理为:在服务器端负责生成图形化验证码,并以 ...
- Java内存区域(运行时数据区域)详解、JDK1.8与JDK1.7的区别
2.1 概述 对Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每个对象的new操作去写配对的delete/free 代码,不容易出现内存泄露和内存溢出的问题.不过,仍然需要Java虚 ...
- XJOI NOIP501/511训练22 ttt学字符串
ttt学字符串 题目大意 大法师ttt 最近在学习字符串,有一天他看到魔力xtx左手拿着A字符串,右手拿着B字符串,两个字符串的长度都是一样的,而且都由abc三种字符构成,大法师ttt的灵力值为C,现 ...
- ()C++中的赋值运算符重载函数(operator=)
本文主要介绍C++中的重载操作符(operator)的相关知识. 概述 1.1 what operator 是C++的一个关键字,它和运算符(如=)一起使用,表示一个运算符重载函数,在理解时可将ope ...