同步上下文(SynchronizationContext)


前言

由于之前对同步上下文并不了解,经过各种资料搜索,梳理排版此篇,算是对该知识点一个梳理。本片文章大部分参考MSDN杂志,并加入了一部分自己的理解,理解不到位的地方还请指出。

MSDN的原文链接


一、简述

1、概念

同步上下文是一种可以将工作单元排队到上下文(主要是不同的线程)的方法。
它的作用通俗来讲就是实现线程之间通讯的。

同步上下文应用于很多场景,比如在WinForms和WPF中,只有一个UI线程可以更新UI元素(文本框,复选框等)。如果尝试从另一个非UI线程更改文本框的内容,则不会发生更改,也可能抛出异常(取决于UI框架)。因此,在这样的应用程序中,非UI线程需要将对UI元素的所有更改安排到UI线程。这就是同步上下文提供的内容。它允许将一个工作单元(执行某些方法)发布到不同的上下文 - 在这种情况下是UI线程。

无论使用哪种平台(ASP.NET 、WinForm 、WPF 等),所有.NET程序都包含同步上下文的概念。Microsoft .NET Framework提供了同步上下文的SynchronizationContext类。根据平台框架不同,又单独提供了WindowsFormsSynchronizationContext(WinForm)类、DispatcherSynchronizationContext(WPF)类等同步上下文的模型但都是继承自SynchronizationContext类。

2、同步上下文的发展

1、原始多线程的环境

  • 多线程程序存在早于 .NET Framework 出现
  • 多线程程序通常需要需要一个线程将工作单元传递给另一线程
  • Windows程序以消息循环为中心,因此许多程序员使用此内置队列来传递工作单元
  • 每个要以这种方式使用 Windows消息队列的多线程程序都必须自定义 Windows 消息以及处理约定

多线程会使事情复杂化的一种非常常见的情况是更新用户界面,由于Winforms线程不安全,因此通常必须从UI线程完成。通过将一些代码从调用线程编组到主UI线程,从而使代码本身在UI线程上执行,从而完全避免同步问题。由于所有与UI相关的工作都在UI线程上完成,因此没有同步问题。

2、ISynchronizeInvoke 的诞生

当 .NET Framework 首次发布时,这一通用模式已经标准化。那时 .NET 唯一支持的 GUI 应用程序类型是 WinFrom。不过,框架设计人员期待其他模型,他们开发出了一种通用的解决方案,ISynchronizeInvoke 诞生了。
ISynchronizeInvoke 的原理是让“源”线程可以将委托排队到“目标”线程队列中,可以选择等待该委托完成。ISynchronizeInvoke还提供了一个属性来确定当前代码是否已在目标线程上运行;在这种情况下,不需要委托排队。
Windows窗体提供了ISynchronizeInvoke的唯一实现,并且开发了一种用于设计异步组件的模式。

3、SynchronizationContext 的诞生

.NET Framework 2.0版包含许多重大更改。主要改进之一是将异步页面引入ASP.NET体系结构。在.NET Framework 2.0之前,每个ASP.NET请求都需要一个线程,直到请求完成为止。这是对线程的低效率使用,因为创建Web页面通常取决于数据库查询和对Web服务的调用,并且处理该请求的线程将不得不等待,直到每个操作都完成为止。对于异步页面,处理请求的线程可以开始每个操作,然后返回到ASP.NET线程池。操作完成后,ASP.NET线程池中的另一个线程将完成请求。

但是,ISynchronizeInvoke不太适合ASP.NET异步页面体系结构。使用ISynchronizeInvoke模式开发的异步组件在ASP.NET页面中无法正常工作,因为ASP.NET异步页面未与单个线程关联。异步页面无需排队工作到原始线程,仅需要维护未完成操作的计数来确定何时可以完成页面请求。经过深思熟虑和精心设计,ISynchronizeInvoke被SynchronizationContext取代。

ISynchronizeInvoke满足两个需求:
• 确定是否需要同步
• 将工作单元从一个线程排队到另一个线程

设计 SynchronizationContext 是为了替代 ISynchronizeInvoke ,但完成设计后,它就不仅仅是一个替代品了。SynchronizationContext与ISynchronizeInvoke 不同,具有以下新特点:

  1. SynchronizationContext提供了一种方式,可以使工作单元列队并列入上下文。
    • 请注意,工作单元是列入上下文,而不是某个特定线程
    • 这一区别非常重要,因为很多 SynchronizationContext 实现都不是基于单个特定线程的
    • SynchronizationContext不包含用来确定是否必须同步的机制,因为这是不可能的。
    • WPF 中的Dispatcher.Invoke是将委托列入上下文,不等委托执行直接返回
    • WinForm 中的txtUName.Invoke会启动一个process,等到委托执行完毕后返回

  2. 每个线程都有当前上下文。
    • 线程上下文不一定唯一;
    其上下文实例可以与多个其他线程共享
    • 线程可以更改其当前上下文,但这样的情况非常少见。

  3. 保留了未完成异步操作的计数。
    • 可以用于 ASP.NET 异步页面和需要此类计数的任何其他主机。
    • 大多数情况下,捕获到当前 SynchronizationContext 时,计数递增;
    • 捕获到的 SynchronizationContext 用于将完成通知列队到上下文中时,计数递减 void OperationCompleted()。

还有其他特点,但是对于大多数程序员而言,它们的重要性并不高。

SynchronizationContext API:

// SynchronizationContext API的重要方面
class SynchronizationContext
{
 
  // 将工作分配到上下文中
  void Post(..); // (asynchronously 异步)
 
  void Send(..); // (synchronously 同步)
 
  // 跟踪异步操作的数量。
  void OperationStarted();
 
  void OperationCompleted();
 
  // 每个线程都有一个Current Context。
  // 如果“Current”为null,则按照惯例,
  // 最开始的当前上下文为 new SynchronizationContext()。
  static SynchronizationContext Current { get; }
 
  //设置当前同步上下文
  static void SetSynchronizationContext(SynchronizationContext);
}

二、同步上下文的实现

不同的框架和主机可以自行定义上下文。通过了解这些不同的实现及其限制,可以清楚了解 SynchronizationContext 概念可以和不可以实现的功能。

1、WinForm 的同步上下文

WindowsFormsSynchronizationContext
命名空间:System.Windows.Forms.dll:System.Windows.Forms

实现:

  • Windows Forms应用程序将创建WindowsFormsSynchronizationContext并将其安装为创建UI控件的任何线程的当前上下文。
  • 此SynchronizationContext在UI控件上使用ISynchronizeInvoke方法,该控件将委托传递到基础Win32消息循环。
  • WindowsFormsSynchronizationContext的上下文是单个UI线程。

排队到WindowsFormsSynchronizationContext的所有委托一次执行一次;它们由特定的UI线程按排队顺序执行。当前实现为每个UI线程创建一个WindowsFormsSynchronizationContext。

2、WPF的同步上下文

DispatcherSynchronizationContext
命名空间:WindowsBase.dll:System.Windows.Threading

实现:

  • Dispatcher的作用是用于管理线程工作项队列,类似于Win32中的消息队列,Dispatcher的内部函数,仍然调用了传统的创建窗口类,创建窗口,建立消息泵等操作。
  • WPF和Silverlight应用程序使用DispatcherSynchronizationContext,该代理将对UI线程的Dispatcher的委托以“Normal”优先级排队。
  • 当线程通过调用Dispatcher.Run开始循环调度器 ,将这个初始化完成的 同步上下文 安装到当前上下文。
  • DispatcherSynchronizationContext的上下文是单个UI线程。

排队到DispatcherSynchronizationContext的所有委托均由特定的UI线程一次按其排队的顺序执行。当前实现为每个顶级窗口创建一个DispatcherSynchronizationContext,即使它们都共享相同的基础Dispatcher。

3、默认同步上下文

(默认)SynchronizationContext
命名空间:mscorlib.dll:System.Threading
调度线程池线程的同步上下文。

实现:

  • 默认SynchronizationContext是默认构造的SynchronizationContext对象。根据惯例,如果线程的当前SynchronizationContext为null,则它隐式具有默认的SynchronizationContext。
  • 默认的同步上下文将异步委托排队到ThreadPool,但直接在调用线程上执行其同步委托。
  • 因此,默认同步上下文涵盖所有ThreadPool线程以及任何调用Send的线程。默认SynchronizationContext“借用”线程调用Send,将它们带入上下文,直到委托完成。从这个意义上讲,默认上下文可以包括进程中的任何线程。

除非代码由ASP.NET托管,否则默认的SynchronizationContext将应用于ThreadPool线程。除非子线程设置自己的SynchronizationContext,否则默认的SynchronizationContext也会隐式应用于显式子线程(Thread类的实例)。因此,UI应用程序通常具有两个同步上下文:覆盖UI线程的UI SynchronizationContext覆盖ThreadPool线程的默认SynchronizationContext

4、ASPNET同步上下文

AspNetSynchronizationContext
命名空间:System.Web.dll:System.Web [internal class]

实现:

  • ASP.NET 同步上下文在执行页面代码时安装在线程池线程上。当委托排队到捕获的AspNetSynchronizationContext时,它将还原原始页面的标识和区域性,然后直接执行委托。即使通过调用Post将其“异步”排队,也可以直接调用该委托。
  • 从概念上讲,AspNetSynchronizationContext的上下文很复杂。在异步页的生存期内,上下文仅从ASP.NET线程池中的一个线程开始。启动异步请求后,上下文不包含任何线程。随着异步请求的完成,执行其完成例程的线程池线程将进入上下文。这些线程可能是发起请求的线程,但更有可能是操作完成时空闲的任何线程。

如果对同一应用程序一次完成多个操作,AspNetSynchronizationContext将确保它们一次执行一个。它们可以在任何线程上执行,但是该线程将具有原始页面的标识和区域性。

一个常见的示例是在异步网页中使用的WebClient。DownloadDataAsync将捕获当前的SynchronizationContext,稍后将在该上下文中执行其DownloadDataCompleted事件。当页面开始执行时,ASP.NET将分配其线程之一以执行该页面中的代码。该页面可以调用DownloadDataAsync然后返回;ASP.NET保留未完成的异步操作的计数,因此它了解页面不完整。WebClient对象下载了请求的数据后,它将在线程池线程上收到通知。该线程将在捕获的上下文中引发DownloadDataCompleted。上下文将保留在同一线程上,但将确保事件处理程序以正确的身份和区域性运行

5、使用上下文捕获和执行的问题

许多基于事件的异步组件无法使用默认的SynchronizationContext正常工作。如,在一个UI应用程序,其中一个BackgroundWorker启动了另一个BackgroundWorker。每个BackgroundWorker捕获并使用调用RunWorkerAsync的线程的SynchronizationContext,然后在该上下文中执行其RunWorkerCompleted事件。

在单个BackgroundWorker的情况下,这通常是基于UI的SynchronizationContext,因此RunWorkerCompleted在RunWorkerAsync捕获的UI上下文中执行。

UI上下文中的单个BackgroundWorker
但是,如果BackgroundWorker从其DoWork处理程序启动另一个BackgroundWorker,则嵌套的BackgroundWorker不会捕获UI SynchronizationContext。DoWork由具有默认SynchronizationContext的ThreadPool线程执行。在这种情况下,嵌套的RunWorkerAsync将捕获默认的SynchronizationContext,因此它将在ThreadPool线程而不是UI线程上执行其RunWorkerCompleted。

在这里插入图片描述
默认情况下,控制台应用程序和Windows Services中的所有线程仅具有默认的SynchronizationContext。这会导致某些基于事件的异步组件失败。
两种解决方案:

6、有关同步上下文实现的说明

SynchronizationContext提供了一种编写可以在许多不同框架中工作的组件的方法。BackgroundWorker和WebClient是Windows Forms,WPF,Silverlight,控制台和ASP.NET应用程序中同样常见的两个示例。

SynchronizationContext实现是无法比较的。
这意味着没有等效的ISynchronizeInvoke.InvokeRequired。代码更简洁,更容易验证它是否始终在已知上下文中执行,而不是尝试处理多个上下文。

并非所有的SynchronizationContext实现都保证委托执行或委托同步的顺序。
基于UI的SynchronizationContext实现确实满足这些条件,但是ASP.NET SynchronizationContext仅提供同步。默认的SynchronizationContext不保证执行顺序或同步顺序。
SynchronizationContext实例与线程之间没有1:1的对应关系。
WindowsFormsSynchronizationContext确实具有到线程的1:1映射(只要不调用SynchronizationContext.CreateCopy即可),但这在任何其他实现中均不成立。通常,最好不要假定任何上下文实例都可以在任何特定线程上运行。

SynchronizationContext.Post方法不一定是异步的。
大多数实现都异步实现它,但是AspNetSynchronizationContext是一个明显的例外。这可能会导致意外的重新进入问题。

SynchronizationContext实现摘要:

使用特定线程 执行委托 独占 (一次执行一个委托) 有序 (委托按队列顺序执行) Send 可以直接调用委托 Post 可以直接调用委托
Winform 如果从UI线程调用 从不
WPF/Silverlight 如果从UI线程调用 从不
Default 总是 从不
ASP.NET 总是 总是

三、AsyncOperationManager和AsyncOperation

NET Framework中的AsyncOperationManager和AsyncOperation类是ynchronizationContext抽象的轻量级封装。本人经常使用Unity,很多时候使用AsyncOperationManager和AsyncOperation实现异步操作。

实现:

  • AsyncOperationManager首次创建AsyncOperation时会捕获当前的SynchronizationContext,如果当前的SynchronizationContext为null,则将其替换为默认的SynchronizationContext。
  • AsyncOperation将委托异步发布到捕获的SynchronizationContext

大多数基于事件的异步组件在其实现中都使用AsyncOperationManager和AsyncOperation。这些方法对于具有定义的完成点的异步操作非常有效,也就是说,异步操作在一个点开始,在另一个点结束。其他异步通知可能没有定义的完成点。这些可能是一种订阅,从一个点开始,然后无限期地继续。对于这些类型的操作,可以直接捕获并使用SynchronizationContext。

新组件不应使用基于事件的异步模式,应采用基于任务的异步模式。组件返回TaskTask<TResult>对象,而不是通过SynchronizationContext引发事件。基于任务的API是.NET中异步编程的未来,不过就目前来说,TAP已经是异步普遍采用的模式了。

四、同步上下文的库支持示例

BackgroundWorker和WebClient之类的简单组件本身就隐式可移植,从而隐藏了同步上下文的捕获和用法。许多库对SynchronizationContext都有更明显的用途。通过使用SynchronizationContext公开API,库不仅获得框架独立性,而且还为高级最终用户提供了可扩展性。

目前的SynchronizationContext被认为是ExecutionContext的一部分,此外ExecutionContext还包括安全上下文调用上下文同步上下文。捕获线程的ExecutionContext的任何系统都将捕获当前的SynchronizationContext。恢复ExecutionContext时,通常也恢复SynchronizationContext。

1、WCF

Windows Communication Foundation(WCF):UseSynchronizationContext

WCF具有两个用于配置服务器和客户端行为的属性:ServiceBehaviorAttribute和CallbackBehaviorAttribute。这两个属性都有一个Boolean 属性:UseSynchronizationContext。此属性的默认值为true,这意味着在创建通信通道时将捕获当前的SynchronizationContext,并且使用此捕获的SynchronizationContext来对协定方法进行排队。

  • 服务器使用默认的SynchronizationContext
  • 客户端回调使用适当的UI SynchronizationContext

但是当需要重新输入时,这可能会引起问题,例如客户端调用调用客户端回调的服务器方法。在这种情况下,可以通过将UseSynchronizationContext设置为false来禁用WCF对SynchronizationContext的自动使用。
参考文档:msdn.microsoft.com/magazine/cc163321

2、WF

Windows Workflow Foundation(WF):WorkflowInstance.SynchronizationContext

WF主机最初使用WorkflowSchedulerService和派生类型来控制如何在线程上计划工作流活动。.NET Framework 4升级的一部分包括WorkflowInstance类及其派生的WorkflowApplication类的SynchronizationContext属性。
如果托管进程创建了自己的WorkflowInstance,则可以直接设置SynchronizationContext。
WorkflowInvoker.InvokeAsync也使用SynchronizationContext,它捕获当前的SynchronizationContext并将其传递给其内部WorkflowApplication。然后,此SynchronizationContext用于发布工作流完成事件以及工作流活动。

3、任务并行库(TPL)

Task Parallel Library (TPL): TaskScheduler.FromCurrentSynchronizationContext 和 CancellationToken.Register

TPL 使用任务(Task)对象作为其工作单元并通过任务调度( TaskScheduler )执行。

  • 默认TaskScheduler的作用类似于默认同步上下文,将任务排队到ThreadPool。
  • TPL队列提供了另一个TaskScheduler,它将任务排队到同步上下文。
  • UI更新的进度报告可以使用嵌套任务来完成。
private void button1_Click(object sender, EventArgs e)
{
  // 捕捉当前上下文的任务调度
  TaskScheduler taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
  // 开始一个新任务(使用默认任务调度)
  // 所以它将运行在一个线程池线程
  Task.Factory.StartNew(() =>
  {
      //运行一个线程池线程
      
    ; 
      //报告UI进度
    Task reportProgressTask = Task.Factory.StartNew(() =>
      {
        // We are running on the UI thread here.
        ; // Update the UI with our progress.
      },
      CancellationToken.None,
      TaskCreationOptions.None,
      taskScheduler);
      
    reportProgressTask.Wait();
  
    ; 
  });
}

CancellationToken类用于.NET Framework 4中的任何类型的取消操作。
为了与现有的取消形式集成,此类允许注册委托以在请求取消时调用。当委托被注册时,可以传递SynchronizationContext。当请求取消时,CancellationToken将委托排队到SynchronizationContext中,而不是直接执行它。

4、Reactive Extensions (Rx)

Microsoft Reactive Extensions(Rx):ObserveOn, SubscribeOn和SynchronizationContextScheduler
Rx是一个库,它将事件视为数据流。

  • ObserveOn(context) 运算符通过一个 同步上下文 将事件列队
  • SubscribeOn(context) 运算符通过一个 同步上下文 将对这些事件的订阅 列队

ObserveOn(context) 通常用于使用传入事件更新 UI,SubscribeOn 用于从 UI 对象使用事件
Rx还具有其自己的工作单元排队方式:IScheduler接口。
Rx包括SynchronizationContextScheduler。

  • 是一个将 Task 列入指定 同步上下文 的 IScheduler 实现
  • 构造方法: SynchronizationContextScheduler(SynchronizationContext context)

5、异步编程 Async

await 、 ConfigureAwait 、 SwitchTo 和 Progress<T>

默认情况下, 当前同步上下文在一个 await 关键字处被捕获,捕获的同步上下文用于在运行到await后时恢复。也就是await关键字后面的执行代码会被列入到该同步上下文中执行。
若捕获当前的SynchronizationContext为NULL,则捕获当前 TaskScheduler。

private async void button1_Click(object sender, EventArgs e)
{
  // 当前 SynchronizationContext 被 await 在暗中捕获
  var data = await webClient.DownloadStringTaskAsync(uri);
 
  // 此时,已捕获的SynchronizationContext用于恢复执行,
  // 因此我们可以自由更新UI对象。
}

ConfigureAwait提供了一种避免默认上下文捕获行为的方法。参数传递false会阻止使用SynchronizationContext在等待之后恢复执行。
在同步上下文实例上还有一个扩展方法,称为SwitchTo。这允许任何异步方法通过调用SwitchTo并等待结果来更改为不同的同步上下文。

异步CTP引入了一种报告异步操作进度的通用模式:IProgress 接口及其实现EventProgress 。此类在构造时捕获当前的SynchronizationContext,并在该上下文中引发其ProgressChanged事件

IProgress 接口及其实现 Progress
• 该类在构造时捕获当前同步上下文
• 并在中引发其ProgressChanged 事件
• 所以实例化时,需要在UI同步上下文上执行

返回 void 的 async 方法
• 在异步操作开始时递增计数
• 在异步操作结束后递减计数
这一行为使返回 void 的 async 方法类似于顶级异步操作。

总结

本文仅仅简单介绍了同步上下文,了解SynchronizationContext对任何程序员都有帮助。现有的跨框架组件使用它来同步其事件。

posted @ 2021-02-05 11:09  20世纪少年  阅读(657)  评论(0编辑  收藏  举报