同步上下文(SynchronizationContext) 和 C#中跨线程更新UI的方法总结

在C#编程环境中,不允许在工作线程中直接对主线程(UI线程)中的控件进行更新操作。因此,稍微复杂一点的程序,跨线程更新UI界面是非常常见的。

目前,一般有以下几种方法可以实现在工作线程对UI控件的更新:

  • 通过UI线程的SynchronizationContext /sɪŋkrənaɪ’zeɪʃ(ə)n ˈkɑːntekst/的Post/Send方法更新
  • 通过UI控件的Invoke/BeginInvoke方法更新
  • 通过BackgroundWorker取代Thread执行异步操作
  • 通过取消线程安全检查来避免"跨线程操作异常"

1 通过UI线程的SynchronizationContext的Post/Send方法更新

英 [ˌsɪŋkrənaɪ’zeɪʃ(ə)n ˈkɒntekst] 同步上下文
使用线程上下文更新UI线程的步骤:

  • 第一步:获取UI线程同步上下文(在窗体构造函数或FormLoad事件中
  • 第二步:定义线程的主体方法
  • 第三步:定义更新UI控件的方法

通过UI线程与工作线程的时序图可以看出整个更新的步骤:

在这里插入图片描述
整个过程中关键的是主线程的SynchronizationContext,SynchronizationContext在通讯中充当传输者的角色。
在线程执行过程中,需要更新到UI控件上的数据不再直接更新,而是通过UI线程上下文的Post/Send方法,将数据以异步/同步消息的形式发送到UI线程的消息队列;
UI线程收到该消息后,根据消息是异步消息还是同步消息来决定通过异步/同步的方式调用SetTextSafePost方法直接更新自己的控件了。

在本质上,向UI线程发送的消息并是不简单数据,而是一条委托调用命令。

关于SynchronizationContext的详细介绍,可以参考这篇文章:利用SynchronizationContext.Current在**

线程间同步上下文**

一、简述

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 诞生了。

  1. ISynchronizeInvoke 的原理是让 “源”线程可以将委托排队到“目标”线程队列中,可以选择等待该委托完成。
  2. ISynchronizeInvoke还提供了一个属性来确定当前代码是否已在目标线程上运行;在这种情况下,不需要委托排队。
  3. 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 不同,具有以下新特点:

SynchronizationContext提供了一种方式,可以使工作单元列队并列入上下文。

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

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

保留了未完成异步操作的计数。
• 可以用于 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
在这里插入图片描述

实现:

  1. Windows Forms应用程序将创建WindowsFormsSynchronizationContext并将其安装为创建UI控件的任何线程的当前上下文。
  2. 此SynchronizationContext在UI控件上使用ISynchronizeInvoke方法,该控件将委托传递到基础Win32消息循环。
  3. WindowsFormsSynchronizationContext的上下文是单个UI线程。
  4. 排队到WindowsFormsSynchronizationContext的所有委托一次执行一次;它们由特定的UI线程按排队顺序执行。
  5. 当前实现为每个UI线程创建一个WindowsFormsSynchronizationContext。
2、WPF的同步上下文

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

在这里插入图片描述
在这里插入图片描述

实现:

  1. Dispatcher的作用是用于管理线程工作项队列,类似于Win32中的消息队列,Dispatcher的内部函数,仍然调用了传统的创建窗口类,创建窗口,建立消息泵等操作。
  2. WPF和Silverlight应用程序使用DispatcherSynchronizationContext,该代理将对UI线程的Dispatcher的委托以“Normal”优先级排队。
  3. 当线程通过调用Dispatcher.Run开始循环调度器 ,将这个初始化完成的 同步上下文 安装到当前上下文。
  4. DispatcherSynchronizationContext的上下文是单个UI线程。
  5. 排队到DispatcherSynchronizationContext的所有委托均由特定的UI线程一次按其排队的顺序执行。
  6. 当前实现为每个顶级窗口创建一个DispatcherSynchronizationContext,即使它们都共享相同的基础Dispatcher。
3、默认同步上下文

(默认)SynchronizationContext
命名空间:mscorlib.dll:System.Threading
调度线程池线程的同步上下文。
在这里插入图片描述

实现:

  1. 默认SynchronizationContext是默认构造的SynchronizationContext对象。根据惯例,如果线程的当前SynchronizationContext为null,则它隐式具有默认的SynchronizationContext。
  2. 默认的同步上下文将异步委托排队到ThreadPool,但直接在调用线程上执行其同步委托。
  3. 因此,默认同步上下文涵盖所有ThreadPool线程以及任何调用Send的线程。默认SynchronizationContext“借用”线程调用Send,将它们带入上下文,直到委托完成。从这个意义上讲,默认上下文可以包括进程中的任何线程。
  4. 除非代码由ASP.NET托管,否则默认的SynchronizationContext将应用于ThreadPool线程。除非子线程设置自己的SynchronizationContext,否则默认的SynchronizationContext也会隐式应用于显式子线程(Thread类的实例)。
  5. 因此,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上下文中执行。

在这里插入图片描述
但是,如果BackgroundWorker从其DoWork处理程序启动另一个BackgroundWorker,则嵌套的BackgroundWorker不会捕获UI SynchronizationContext。DoWork由具有默认SynchronizationContext的ThreadPool线程执行。在这种情况下,嵌套的RunWorkerAsync将捕获默认的SynchronizationContext,因此它将在ThreadPool线程而不是UI线程上执行其RunWorkerCompleted。
在这里插入图片描述
默认情况下,控制台应用程序和Windows Services中的所有线程仅具有默认的SynchronizationContext。这会导致某些基于事件的异步组件失败。
两种解决方案:

创建一个显式子线程并在该线程上安装SynchronizationContext,然后可以为这些组件提供上下文。
实现SynchronizationContext,可参考Nito.Async库(nitoasync.codeplex.com或https://github.com/StephenClearyArchive/Nito.Asynchronous)的ActionThread类可以用作通用的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实现摘要:

三、AsyncOperationManager和AsyncOperation

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

实现:

AsyncOperationManager首次创建AsyncOperation时会捕获当前的SynchronizationContext,如果当前的SynchronizationContext为null,则将其替换为默认的SynchronizationContext。
AsyncOperation将委托异步发布到捕获的SynchronizationContext。
大多数基于事件的异步组件在其实现中都使用AsyncOperationManager和AsyncOperation。这些方法对于具有定义的完成点的异步操作非常有效,也就是说,异步操作在一个点开始,在另一个点结束。其他异步通知可能没有定义的完成点。这些可能是一种订阅,从一个点开始,然后无限期地继续。对于这些类型的操作,可以直接捕获并使用SynchronizationContext。

新组件不应使用基于事件的异步模式,应采用基于任务的异步模式。组件返回Task和Task对象,而不是通过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 )执行。

  1. 默认TaskScheduler的作用类似于默认同步上下文,将任务排队到ThreadPool。
  2. TPL队列提供了另一个TaskScheduler,它将任务排队到同步上下文。
  3. 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

默认情况下, 当前同步上下文在一个 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对任何程序员都有帮助。现有的跨框架组件使用它来同步其事件。
from:https://www.cnblogs.com/craft0625/p/14376557.html

2. 通过UI控件的Invoke/ BeginInvoke方法更新

通过UI控件的Inovke/BeginInvoke方法同样也可以实现更新,具体步骤如下:

在这里插入图片描述
这个方法是目前跨线程更新UI使用的主流方法,使用控件的Invoke/BegainInvoke方法,将委托转到UI线程上调用,实现线程安全的更新。原理与方法1类似,本质上还是把线程中要提交的消息,通过控件句柄调用委托交到UI线程中去处理。

3. 通过BackgroundWorker取代Thread执行异步操作

BackgroundWorker组件是.Net推出的一个组件,用来方便地进行跨线程的界面更新操作。以下引自MSDN:

BackgroundWorker 类允许您在单独的专用线程上运行操作。
耗时的操作(如下载和数据库事务)在长时间运行时可能会导致用户界面 (UI) 似乎处于停止响应状态。如果您需要能进行响应的用户界面,而且面临与这类操作相关的长时间延迟,则可以使用 BackgroundWorker 类方便地解决问题。

具体可参见MSDN的描述:BackgroundWorker

4. 通过取消线程安全检查来避免"跨线程操作异常"

将Control类的静态属性CheckForIllegalCrossThreadCalls为false,这种方式简单粗暴,但是因为取消了跨线程的检查,可能会引起一些异常问题,因此不推荐使用。

三、 SynchronizationContext 的概念

ISynchronizeInvoke 满足了两点需求:确定是否必须同步,使工作单元从一个线程列队等候另一个线程。设计 SynchronizationContext 是为了替代 ISynchronizeInvoke,但完成设计后,它就不仅仅是一个替代品了。

一方面,SynchronizationContext 提供了一种方式,可以使工作单元列队列入上下文。请注意,工作单元是列入上下文,而不是某个特定线程。这一区别非常重要,因为很多 SynchronizationContext 实现都不是基于单个特定线程的。SynchronizationContext 不包含用来确定是否必须同步的机制,因为这是不可能的。

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

SynchronizationContext 的第三个方面是它保持未完成操作的计数。这样,就可以使用 ASP.NET 异步页面和需要此类计数的任何其他主机。大多数情况下,捕获到当前 SynchronizationContext 时,计数递增;捕获到的 SynchronizationContext 用于将完成通知列队到上下文中时,计数递减。

SynchronizationContext API 的各方面

// The important aspects of the SynchronizationContext APIclass SynchronizationContext

{

  // Dispatch work to the context.
void Post(..); // (asynchronously)

  void Send(..); // (synchronously)

  // Keep track of the number of asynchronous operations.
void OperationStarted();

  void OperationCompleted();

  // Each thread has a current context.
// If "Current" is null, then the thread's current context is


  // "new SynchronizationContext()", by convention.
static SynchronizationContext Current { get; }

  static void SetSynchronizationContext(SynchronizationContext);
}
SynchronizationContext 的实现

SynchronizationContext 的实际“上下文”并没有明确的定义。不同的框架和主机可以自行定义自己的上下文。通过了解这些不同的实现及其限制,可以清楚了解 SynchronizationContext 概念可以和不可以实现的功能。我将简单讨论部分实现。

WindowsFormsSynchronizationContext(System.Windows.Forms.dll:System.Windows.Forms)Windows 窗体应用程序会创建并安装一个 WindowsFormsSynchronizationContext 作为创建 UI 控件的任意线程的当前上下文。这一 SynchronizationContext 使用 UI 控件的 ISynchronizeInvoke 方法,该方法将委托传递给基础 Win32 消息循环。WindowsFormsSynchronizationContext 的上下文是一个单独的 UI 线程。

在 WindowsFormsSynchronizationContext 列队的所有委托一次一个地执行;它们通过一个特定 UI 线程执行以便列队。当前实现为每个 UI 线程创建一个 WindowsFormsSynchronizationContext。

DispatcherSynchronizationContext(WindowsBase.dll:System.Windows.Threading)WPF 和 Silverlight 应用程序使用 DispatcherSynchronizationContext,这样,委托按“常规”优先级在 UI 线程的调度程序中列队。当一个线程通过调用 Dispatcher.Run 开始其调度程序时,这一 SynchronizationContext 作为当前上下文安装。DispatcherSynchronizationContext 的上下文是一个单独的 UI 线程。

在 DispatcherSynchronizationContext 列队的所有委托一次一个地执行;它们通过一个特定 UI 线程执行以便列队。当前实现为每个顶层窗口创建一个 DispatcherSynchronizationContext,即使它们都使用相同的基础调度程序也是如此。

默认 (ThreadPool) SynchronizationContext(mscorlib.dll:System.Threading)默认 SynchronizationContext 是默认构造的 SynchronizationContext 对象。根据惯例,如果一个线程的当前 SynchronizationContext 为 null,那么它隐式具有一个默认 SynchronizationContext。

默认 SynchronizationContext 将其异步委托列队到 ThreadPool,但在调用线程上直接执行其同步委托。因此,其上下文包含所有 ThreadPool 线程以及调用 Send 的任何线程。此上下文“借用”调用 Send 的线程,将它们放入其上下文,直至委托完成。从这种意义上讲,默认上下文可以包含进程中的所有 线程。

默认 SynchronizationContext 应用于 ThreadPool 线程,除非代码由 ASP.NET 承载。默认 SynchronizationContext 还隐式应用于显式子线程(Thread 类的实例),除非子线程设置自己的 SynchronizationContext。因此,UI 应用程序通常有两个同步上下文:包含 UI 线程的 UI SynchronizationContext 和包含 ThreadPool 线程的默认 SynchronizationContext。

很多基于事件的异步组件使用默认 SynchronizationContext 无法正常工作。一个 BackgroundWorker 启动另一个 BackgroundWorker 这样的 UI 应用程序就是一个糟糕的示例。每个 BackgroundWorker 都捕获并使用调用 RunWorkerAsync 的线程的 SynchronizationContext,之后在该上下文中执行其 RunWorkerCompleted 事件。在只有一个 BackgroundWorker 的情况下,这通常是基于 UI 的 SynchronizationContext,因此 RunWorkerCompleted 在 RunWorkerAsync 捕获的 UI 上下文中执行(请参见图 2)。

SynchronizationContext 的库支持示例

像 BackgroundWorker 和 WebClient 这样的简单组件是隐式自带的,隐藏了 SynchronizationContext 捕获和使用。很多库以更可见的方式使用 SynchronizationContext。通过使用 SynchronizationContext 公开 API,库不仅获得了框架独立性,而且为高级最终用户提供了一个可扩展点。

除了下面讨论的库,当前 SynchronizationContext 也被视为 ExecutionContext 的一部分。任何捕获线程的 ExecutionContext 的系统都会捕获当前 SynchronizationContext。当恢复 ExecutionContext 时,通常也会恢复 SynchronizationContext。

Windows Communication Foundation (WCF):UseSynchronizationContext WCF 有两个用于配置服务器和客户端行为的特性:ServiceBehaviorAttribute 和 CallbackBehaviorAttribute。这两个特性都有一个 Boolean 属性:UseSynchronizationContext。此特性的默认值为 true,这表示在创建通信通道时捕获当前 SynchronizationContext,这一捕获的 SynchronizationContext 用于使约定方法列队。

通常,这一行为正是我们所需要的:服务器使用默认 SynchronizationContext,客户端回调使用相应的 UI SynchronizationContext。在需要重入时,这会导致问题,如客户端调用的服务器方法调用一个客户端回调。在这类情况下,将 UseSynchronizationContext 设置为 false 可以禁止 WCF 自动使用 SynchronizationContext。

上面只是简单介绍了 WCF 如何使用 SynchronizationContext。有关详细信息,请参阅 MSDN 杂志 2007 年 11 月刊中的文章“WCF 中的同步上下文”(msdn.microsoft.com/magazine/cc163321)。

Windows Workflow Foundation (WF):WorkflowInstance.SynchronizationContext WF 主机最初使用 WorkflowSchedulerService 和派生类型控制如何在线程上安排工作流活动。部分 .NET Framework 4 升级在 WorkflowInstance 类及其派生类 derived WorkflowApplication 上包含 SynchronizationContext 属性。

如果承载进程创建自己的 WorkflowInstance,则可以直接设置 SynchronizationContext。WorkflowInvoker.InvokeAsync 也使用 SynchronizationContext,它捕获当前 SynchronizationContext 并将其传递给其内部 WorkflowApplication。然后该 SynchronizationContext 用于发布工作流完成事件以及工作流活动。

任务并行库 (TPL):TaskScheduler.FromCurrentSynchronizationContext 和 CancellationToken.Register TPL 使用任务对象作为其工作单元并通过 TaskScheduler 执行。默认 TaskScheduler 的作用类似于默认 SynchronizationContext,将任务在 ThreadPool 中列队。TPL 队列还提供了另一个 TaskScheduler,将任务在 SynchronizationContext 中列队。UI 更新的进度报告可以在一个嵌套任务中完成,如图 5 所示。

图 5 UI 更新的进度报告

private void button1_Click(object sender, EventArgs e)
{
  // This TaskScheduler captures SynchronizationContext.Current.
TaskScheduler taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
  // Start a new task (this uses the default TaskScheduler, 
  // so it will run on a ThreadPool thread).
Task.Factory.StartNew(() =>
  {
    // We are running on a ThreadPool thread here.
; // Do some work.
// Report progress to the 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();
  
    ; // Do more work.
});
}

在 .NET Framework 4 中,CancellationToken 类可用于任意类型的取消操作。 为了与现有取消操作形式集成,该类允许注册委托以在请求取消时调用。 注册委托后,就可以传递 SynchronizationContext 了。 如果出现取消请求,CancellationToken 将该委托列入 SynchronizationContext 队列而不是直接执行它。

Microsoft 被动扩展 (Rx):ObserveOn、 SubscribeOn 和 SynchronizationContextScheduler Rx 是一个库,它将事件视为数据流。 ObserveOn 运算符通过一个 SynchronizationContext 将事件列队,SubscribeOn 运算符通过一个 SynchronizationContext 将对这些事件的订阅 列队。 ObserveOn 通常用于使用传入事件更新 UI,SubscribeOn 用于从 UI 对象使用事件。

Rx 还有它自己的工作单元列队方法:IScheduler 接口。 Rx 包含 SynchronizationContextScheduler,这是一个列入 SynchronizationContext 的 IScheduler 实现。

Visual Studio Async CTP:await、ConfigureAwait、 SwitchTo 和 EventProgress Visual Studio 对异步代码转换的支持是在 2010 年 Microsoft 专业开发人员大会上发布的。 默认情况下,当前 SynchronizationContext 在一个等待点捕获,此 SynchronizationContext 用于在等待后继续(更确切地说,仅当它不为 null 时,才捕获当前 SynchronizationContext,如果为 null,则捕获当前 TaskScheduler):

private async void button1_Click(object sender, EventArgs e)
{
  // SynchronizationContext.Current is implicitly captured by await.
var data = await webClient.DownloadStringTaskAsync(uri);

  // At this point, the captured SynchronizationContext was used to resume
  // execution, so we can freely update UI objects.
}
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐