一般情况下,我们可以将某项操作分发给任意线程来执行,但有的操作确实对于执行的线程是有要求的,最为典型的场景就是: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]:同步上下文的更多相关文章

  1. 从执行上下文角度重新理解.NET(Core)的多线程编程[1]:基于调用链的”参数”传递

    线程是操作系统能够进行运算调度的最小单位,操作系统线程进一步被封装成托管的Thread对象,手工创建并管理Thread对象已经成为了所能做到的对线程最细粒度的控制了.后来我们有了ThreadPool, ...

  2. 执行上下文与同步上下文 | ExecutionContext 和 SynchronizationContext

    原文连接:执行上下文与同步上下文 - .NET 并行编程 (microsoft.com) 执行上下文与同步上下文 斯蒂芬 6月15日, 2012 最近,我被问了几次关于 ExecutionContex ...

  3. SynchronizationContext(同步上下文)综述

    >>返回<C# 并发编程> 1. 概述 2. 同步上下文 的必要性 2.1. ISynchronizeInvoke 的诞生 2.2. SynchronizationContex ...

  4. [翻译 EF Core in Action 2.3] 理解EF Core数据库查询

    Entity Framework Core in Action Entityframework Core in action是 Jon P smith 所著的关于Entityframework Cor ...

  5. 【C# Task】理解Task中的ConfigureAwait配置同步上下文

    原文:https://devblogs.microsoft.com/dotnet/configureawait-faq/ 作者:Stephen 翻译:xiaoxiaotank 静下心来,你一定会有收获 ...

  6. Android AsyncTask完全解析,带你从源码的角度彻底理解

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/11711405 我们都知道,Android UI是线程不安全的,如果想要在子线程里进 ...

  7. 从逆向的角度去理解C++虚函数表

    很久没有写过文章了,自己一直是做C/C++开发的,我一直认为,作为一个C/C++程序员,如果能够好好学一下汇编和逆向分析,那么对于我们去理解C/C++将会有很大的帮助,因为程序中所有的奥秘都藏在汇编中 ...

  8. [转]Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

    Android事件分发机制 该篇文章出处:http://blog.csdn.net/guolin_blog/article/details/9097463 其实我一直准备写一篇关于Android事件分 ...

  9. 从源码角度深入理解Handler

    为了获得良好的用户体验,Android不允许开发者在UI线程中调用耗时操作,否则会报ANR异常,很多时候,比如我们要去网络请求数据,或者遍历本地文件夹都需要我们在新线程中来完成,新线程中不能更新UI, ...

随机推荐

  1. 【踩坑系列】使用long类型处理金额,科学计数法导致金额转大写异常

    1. 踩坑经历 上周,一个用户反馈他创建的某个销售单无法打开,但其余销售单都可以正常打开,当时查看了生产环境的ERROR日志,发现抛了这样的异常:java.lang.NumberFormatExcep ...

  2. No compatible servers were found,You'll need to cancel this wizard and install one!

    原文链接:https://www.jianshu.com/p/a11f93fb16ce 问题原因 笔记本重装的windows系统,重新安装mysql的时候,显示错误,看了一下缺失服务,实际上可能是缺少 ...

  3. WinSocket01

    启动windows平台下的Socket 1 #define WIN32_LEAN_AND_MEAN 2 #include<windows.h> 3 #include<WinSock2 ...

  4. python机器学习实现K-近邻算法(KNN)

    机器学习 K-近邻算法(KNN) 关注公众号"轻松学编程"了解更多. 以下命令都是在浏览器中输入. cmd命令窗口输入:jupyter notebook 后打开浏览器输入网址htt ...

  5. Photoshop如何安装蓝湖插件

    Photoshop如何安装蓝湖插件 下载蓝湖插件 直通车:蓝湖Photoshop插件: Photoshop版本要求为cc2017以上, 下载后是一个zip格式的文件,我们需要解压. 下载的文件 解压后 ...

  6. Java_流相关

    java.io包中重要的5个类3个接口 类名 说明 File 文件类 InputStream 字节流输入 OutputStream 字节流输出 Reader 字符输入流 Writer 字符输出流 Cl ...

  7. Loading descriptor for XXX.'has encountered a problem' A internal error occured during:"Loading ....."

    在JavaWeb部署Tomcat后启动Jsp发现这样的报错 这可能是Tomcat的运行欢迎有问题,按下图所示打开Tomcat界面.Servers目录就是当前工作空间对所有工程适用的的Tomcat环境, ...

  8. Redis学习(一)——初识Redis

    1.Redis是什么 1)REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统. 2)Redis的特点 Red ...

  9. Linux杂谈: 实现一种简单实用的线程池(C语言)

    基本功能 1. 实现一个线程的队列,队列中的线程启动后不再释放: 2. 没有任务执行时,线程处于pending状态,等待唤醒,不占cpu: 3. 当有任务需要执行时,从线程队列中取出一个线程执行任务: ...

  10. 安全也挺让人心烦的 ---login shell

    今天查问题时, ssh 登录后台发现 需要输入密码,输入密码后弹出一个二维码, 然后扫码获取秘钥.输入秘钥登陆!!! 真是恶心了一把,找手机都花了不少时间!!!! 那么怎样干掉输入密码这些操作呢??? ...