一般情况下,我们可以将某项操作分发给任意线程来执行,但有的操作确实对于执行的线程是有要求的,最为典型的场景就是: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. Percona Toolkit工具连接MySQL 8报错的解决方案

    使用Percona Toolkit的工具连接MySQL 8.x数据库时,会遇到类似"failed: Plugin caching_sha2_password could not be loa ...

  2. [论文阅读]阿里DIEN深度兴趣进化网络之总体解读

    [论文阅读]阿里DIEN深度兴趣进化网络之总体解读 目录 [论文阅读]阿里DIEN深度兴趣进化网络之总体解读 0x00 摘要 0x01论文概要 1.1 文章信息 1.2 基本观点 1.2.1 DIN的 ...

  3. SQL Server 列存储索引 第三篇:维护

    列存储索引分为两种类型:聚集的列存储索引和非聚集的列存储索引,在一个表上只能创建一个聚集索引,要么是聚集的列存储索引,要么是聚集的行存储索引,然而一个表上可以创建多个非聚集索引. 一,创建列存储索引 ...

  4. CF1336 Linova and Kingdom

    题面 给定 n 个节点的有根树,根是 1 号节点. 你可以选择 k 个节点将其设置为工业城市,其余设置为旅游城市. 对于一个工业城市,定义它的幸福值为工业城市到根的路径经过的旅游城市的数量. 你需要求 ...

  5. Java学习的第四十七天

    1.用类函数来写时间类 import java.util.Scanner; public class Cjava { public static void main(String[]args) { T ...

  6. 计算机网络:TCP协议建立连接的过程为什么是三次握手而不是两次?【对于网上的两种说法我的思考】

    网上关于这个问题吵得很凶,但是仔细看过之后我更偏向认为两种说的是一样的. 首先我们来看看 TCP 协议的三次握手过程 如上图所示: 解释一下里面的英文: 里面起到作用的一些标志位就是TCP报文首部里的 ...

  7. MapReduce在Shuffle阶段按Mapper输出的Value进行排序

    ZKe ----------------- 在MapReduce框架中,Mapper的输出在Shuffle阶段,根据Key值分组之后,还将会根据Key值进行排序,因此Reducer的输出我们看到的结果 ...

  8. ubuntu下安装nginx -php

    mysql : sudo apt-get install mysql-server mysql-client nginx: sudo apt-get install nginx安装Nginx稳定版本 ...

  9. UGC

    文章目录    4 UGC推荐        4.1 基于用户UGC标签进行推荐            4.1.1 最简单的算法(SimpleTagBased)            4.1.2 利用 ...

  10. Java中float浮点型变量不加F报错情况

    1 public class Text { 2 3 public static void main(String args[] ){ 4 float x=123.45; 5 System.out.pr ...