SynchronizationContext(同步上下文)綜述


>>返回《C# 並發編程》

1. 概述

注意: 本篇文章講述的是在 .Net Framework 環境下的分析, 但是我相信這與 .Net Core 設計思想是一致,但在實現上一定優化了很多。

下面開始本次講述:

無論是什么平台(ASP.NET 、WinForm 、WPF 等),所有 .NET 程序都包含 同步上下文 概念,並且所有多線程編程人員都可以通過理解和應用它獲益。

2. 同步上下文 的必要性

2.1. ISynchronizeInvoke 的誕生

  • 原始多線程

    • 多線程程序在 .NET Framework 出現之前就存在了。
    • 這些程序通常需要一個線程將一個工作單元傳遞給另一個線程
    • Windows 程序圍繞消息循環進行,因此很多編程人員使用這一內置隊列傳遞工作單元
    • 每個要以這種方式使用 Windows 消息隊列的多線程程序都必須自定義 Windows 消息以及處理約定
  • ISynchronizeInvoke 的誕生

    • .NET Framework 首次發布時,這一通用模式是標准化模式。
    • 那時 .NET 唯一支持的 GUI 應用程序類型是 WinFrom。
    • 不過,框架設計人員期待其他模型,他們開發出了一種通用的解決方案,ISynchronizeInvoke 誕生了。
  • ISynchronizeInvoke 的原理

    • 一個“源”線程可以將一個委托列入“目標”線程隊列。
    • ISynchronizeInvoke 還提供了一個屬性來確定當前代碼是否已在目標線程上運行。
    • WinForm 提供了單例的 ISynchronizeInvoke 實現,並且開發了一種模式來設計異步組件

2.2. SynchronizationContext 的誕生

  • ASP.NET 異步頁面
    • .NET Framework 2.0 版包含很多重大改動。 其中一項重要改進是在 ASP.NET 體系結構中引入了異步頁面
      • 在 .NET Framework 2.0 之前的版本中,每個 ASP.NET 請求都需要一個線程,線程會直到該請求完成
      • 這會造成線程利用率低下,因為創建網頁通常依賴於數據庫查詢和 Web 服務調用,並且處理請求的線程必須等待,直到所有這些操作結束。
      • 后來使用異步頁面,處理請求的線程可以開始每個操作,然后返回到 ASP.NET 線程池;當操作結束時,ASP.NET 線程池的另一個線程可以完成該請求
    • ISynchronizeInvoke 不太適合 ASP.NET 異步頁面體系結構。
      • 使用 ISynchronizeInvoke 模式開發的異步組件在 ASP.NET 頁面內無法正常工作,因為 ASP.NET 異步頁面不與單個線程關聯。
      • 需要設計出,無須將工作排入原來的線程隊列,異步頁面只需對未完成的操作進行計數 以確定頁面請求何時可以完成。
  • 經過精心設計, SynchronizationContext 取代了 ISynchronizeInvoke

3. 同步上下文 的概念

ISynchronizeInvoke 滿足了兩點需求:

  1. 確定是否必須同步
  2. 使工作單元從一個線程列隊等候另一個線程。

設計 SynchronizationContext 是為了替代 ISynchronizeInvoke ,但完成設計后,它就不僅僅是一個替代品了。

  • 一方面SynchronizationContext 提供了一種方式,可以使工作單元列隊並列入上下文
    • 請注意,工作單元是列入上下文,而不是某個特定線程。
    • 這一區別非常重要,因為很多 SynchronizationContext 實現都不是基於單個特定線程的。
    • SynchronizationContext 不包含用來確定是否必須同步的機制,因為這是不可能的。
      • WPF 中的Dispatcher.Invoke是將委托列入上下文,不等委托執行直接返回
      • WinForm 中的txtUName.Invoke會啟動一個process,等到委托執行完畢后返回
  • 另一方面,每個線程都有當前同步上下文
    • 線程上下文不一定唯一
    • 上下文實例可以與多個其他線程共享
    • 線程可以更改其當前上下文,但這樣的情況非常少見。
  • 第三個方面,保持了未完成操作的計數。
    • 這樣,就可以用於 ASP.NET 異步頁面和需要此類計數的任何其他主機。
    • 大多數情況下,捕獲到當前 SynchronizationContext 時,計數遞增
      • 捕獲到的 SynchronizationContext 用於將完成通知列隊到上下文中時,計數遞減 void OperationCompleted()
  • 其他一些方面,這些對大多數編程人員來說並不那么重要
// 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);
}

4. 同步上下文 的實現

不同的框架和主機可以自行定義上下文

通過了解這些不同的實現及其限制,可以清楚了解 SynchronizationContext 概念可以不可以實現的功能

4.1. WinForm 同步上下文

位於:System.Windows.Forms.dll:System.Windows.Forms

WinForm

  • WinForm應用程序會創建並安裝一個 WindowsFormsSynchronizationContext
    • 作為創建 UI Control 的每個線程的當前上下文
    • 一個 WinForm 應用程序對應一個同步上下文
  • 這一 SynchronizationContext 使用 UI ControlInvoke 等方法(ISynchronizeInvoke派生出來的),該方法將委托傳遞給基礎 Win32 消息循環
  • WindowsFormsSynchronizationContext 的上下文是一個單例的 UI 線程
  • WindowsFormsSynchronizationContext 列隊的所有委托一次一個地執行
    • 這個已排序的委托隊列,被一個特定 UI 線程執行完

4.2. Dispatcher 同步上下文

位於:WindowsBase.dll:System.Windows.Threading

WPF

  • 委托按“Normal”優先級在 UI 線程的 Dispatcher 中列隊
  • 當一個線程通過調用 Dispatcher.Run 開啟 循環調度器 時,將這個初始化完成的 同步上下文 安裝到當前上下文
  • DispatcherSynchronizationContext 的上下文是一個單獨的 UI 線程。
  • 排隊到 DispatcherSynchronizationContext 的所有委托均由特定的UI線程一次一個按其排隊的順序執行
  • 當前實現為每個頂層窗口創建一個 DispatcherSynchronizationContext,即使它們都使用相同的基礎調度程序也是如此。

4.3. Default 同步上下文

調度線程池線程的同步上下文。

位於:mscorlib.dll:System.Threading

Default SynchronizationContext 是默認構造的 SynchronizationContext 對象。

  • 根據慣例,如果一個線程的當前 SynchronizationContext 為 null,那么它隱式具有一個Default SynchronizationContext
  • Default SynchronizationContext 將其異步委托列隊到 ThreadPool ,但在調用線程上直接執行其同步委托
  • 因此,Default SynchronizationContext涵蓋所有 ThreadPool 線程以及任何調用 Send 的線程。
  • 這個上下文“借助”調用 Send線程們,將這些線程放入這個上下文,直至委托執行完成
    • 從這種意義上講,默認上下文可以包含進程中的所有線程
  • Default SynchronizationContext 應用於 線程池 線程,除非代碼由 ASP.NET 承載。
  • Default SynchronizationContext 還隱式應用於顯式子線程(Thread 類的實例),除非子線程設置自己的 SynchronizationContext

因此,UI 應用程序通常有兩個同步上下文:

  • 包含 UI 線程的 UI SynchronizationContext
  • 包含 ThreadPool 線程的Default SynchronizationContext

4.4. 上下文捕獲和執行

BackgroundWorker運行流程

  • 首先BackgroundWorker 捕獲使用調用 RunWorkerAsync 的線程的 同步上下文
  • 然后,在Default SynchronizationContext中執行DoWork
  • 最后,在之前捕獲的上下文中執行其 RunWorkerCompleted 事件

UI同步上下文 中只有一個 BackgroundWorker ,因此 RunWorkerCompletedRunWorkerAsync 捕獲UI同步上下文中執行(如下圖)。

UI同步上下文中的嵌套 BackgroundWorker

  • 嵌套: BackgroundWorker 從其 DoWork 處理程序啟動另一個 BackgroundWorker
    • 嵌套的 BackgroundWorker 不會捕獲 UI同步上下文
  • DoWork線程池 線程使用 默認同步上下文 執行。
    • 在這種情況下,嵌套的 RunWorkerAsync 將捕獲默認 SynchronizationContext
    • 因此它將由一個 線程池 線程而不是 UI線程 執行其 RunWorkerCompleted
    • 這樣會導致異步執行完后,后面的代碼就不在UI同步上下文中執行了(如下圖)。

默認情況下,控制台應用程序Windows服務 中的所有線程都只有 Default SynchronizationContext,這會導致一些基於事件異步組件失敗(也就是沒有UI同步上下文的特性)

  • 要解決這個問題,可以創建一個顯式子線程,然后將 UI同步上下文 安裝在該線程上,這樣就可以為這些組件提供上下文。
  • Nito.Async 庫的 ActionThread 類可用作通用同步上下文實現。

4.5. AspNetSynchronizationContext

位於:System.Web.dll:System.Web [internal class]

ASP.NET

  • SynchronizationContext線程池線程執行頁面代碼安裝完成。
  • 當一個委托列入到捕獲AspNetSynchronizationContext 中時,它設置原始頁面的 identity 和 culture 到此線程,然后直接執行委托
    • 即使委托是通過調用 Post “異步”列入的,也會直接調用委托。

從概念上講, AspNetSynchronizationContext 的上下文非常復雜。

  • 在異步頁面的生命周期中,該同步上下文從來自 ASP.NET 線程池的一個線程開始。
    • 異步請求開始后,該上下文不包含任何線程。
    • 異步請求結束時,線程池線程進入該上下文並執行 處理完成的相關工作
  • 這可能是啟動請求的線程,但更可能是操作完成時處於空閑狀態的任何線程
  • 如果同一應用程序的多項操作同時完成, AspNetSynchronizationContext 確保一次只執行其中一項。它們可以在任意線程上執行,但該線程將具有原始頁面的 identity 和 culture。

一個常見的示例:

在異步網頁中使用 WebClient.DownloadDataAsync 將捕獲當前 SynchronizationContext ,之后在該上下文中執行其 DownloadDataCompleted 事件。

  • 當頁面開始執行時,ASP.NET 會分配一個線程執行該頁面中的代碼。
  • 該頁面可能調用 DownloadDataAsync ,然后返回;
    • ASP.NET 對未完成的異步操作進行計數,以便了解頁面處理是否已完成。
  • WebClient 對象下載所請求的數據后,它將在線程池線程上收到通知
    • 該線程將在捕獲的上下文中引發 DownloadDataCompleted
  • 該上下文將保持在相同的線程中,但會確保事件處理的運行使用正確的 identity 和 culture 運行

5. 同步上下實現類 的注意事項

  • SynchronizationContext 提供了一種途徑,可以在很多不同框架中編寫組件

    • BackgroundWorkerWebClient 就是兩個在 WinFormWPFConsoleASP.NET Application中同樣應用自如的組件。
  • 在設計這類可重用組件時,必須注意幾點:

    • 同步上下文的實現們不是平等可比的。

      • 這意味着沒有類似 ISynchronizeInvoke.InvokeRequired 的等效項
        • 此屬性確定在對如Concrol對象進行方法調用時,調用方是否必須通過 Invoke 進行調用(傳入委托)。
        • 這樣的(Control)對象被綁定到特定線程,並且不是線程安全的。
        • 如果要從其他線程調用對象的方法,則必須借助 Invoke 方法將對相應線程調用的委托列隊
      • 不過,這不是多大的缺點;代碼更為清晰,並且更容易驗證它是否始終在已知上下文中執行,而不是試圖處理多個上下文。
    • 不是所有 同步上下文的實現 都可以保證委托執行順序或委托同步順序。

      • UI同步上下文 滿足上述條件
      • ASP.NET同步上下文 只提供同步
      • Default同步上下文 不保證執行順序或同步順序
    • 同步上下文實例線程之間沒有 1:1 的對應關系

      • WindowsFormsSynchronizationContext 確實 1:1 映射到一個線程(只要不調用 SynchronizationContext.CreateCopy
        • 任何其他實現都不是這樣
      • 一般而言,最好不要假設任何上下文實例將在任何指定線程上運行
    • SynchronizationContext.Post 方法不一定是異步的

      • 大多數實現異步實現此方法,但 AspNetSynchronizationContext 是一個明顯的例外
      • 這可能會導致無法預料的重入問題

同步上下文實現類的摘要

使用特定線程 執行委托 獨占 (一次執行一個委托) 有序 (委托按隊列順序執行) Send 可以直接調用委托 Post 可以直接調用委托
Winform 如果從UI線程調用 從不
WPF/Silverlight 如果從UI線程調用 從不
Default 不能 不能 不能 Always 從不
ASP.NET 不能 不能 Always Always

6. AsyncOperationManager 和 AsyncOperation

  • AsyncOperationManagerAsyncOperation 類是 SynchronizationContext 抽象類的輕型包裝
    • AsyncOperation的異步是使用抽象的同步上下文進行封裝的
  • AsyncOperationManager 在第一次創建 AsyncOperation捕獲當前同步上下文 ,如果當前同步上下文為null,則使用 Default 同步上下文
  • AsyncOperation 將委托異步發布到捕獲的 同步上下文
  • 大多數基於事件的異步組件都在其實現中使用 AsyncOperationManagerAsyncOperation
    • 這些對於具有明確完成點的異步操作非常有效
      • 即異步操作從一個點開始,以另一個點的事件結束
    • 其他異步通知可能沒有明確的完成點;它們可能是一種訂閱類型,在一個點開始,然后無限期持續
      • 對於這些類型的操作,當觸發了被訂閱的事件,在事件處理中直接捕獲使用同步上下文

新組件不應使用基於事件的異步模式

  • 使用基於Task的異步模式
    • 組件返回 Task 和 Task 對象,而不是通過 同步上下文 引發事件
    • 基於 Task 的 API 是 .NET 中異步編程的發展方向

7. 同步上下文 的Library支持示例

  • BackgroundWorkerWebClient 這樣的簡單組件是隱式自帶的
    • 隱藏了對同步上下文的捕獲和使用。
  • 很多 Libraries 以更可見的方式使用 同步上下文
    • 通過使用 SynchronizationContext 公開 API,Libraries 不僅獲得了框架獨立性,而且為高級最終用戶提供了一個可擴展點。
  • ExecutionContext
    • 是與執行的邏輯線程相關的所有信息提供單個容器。 這包括安全上下文調用上下文同步上下文
    • 任何捕獲線程的 ExecutionContext 的系統都會捕獲當前 同步上下文
    • 當恢復 ExecutionContext 時,通常也會恢復 同步上下文

7.1. WCF

WCF 有兩個用於配置服務器和客戶端行為的特性:

  • ServiceBehaviorAttributeCallbackBehaviorAttribute
    • 這兩個特性都有一個 Boolean 屬性:UseSynchronizationContext
    • 此特性的默認值為 true,這表示在創建通信通道時捕獲當前 同步上下文 ,這一捕獲的 同步上下文 用於使約定方法列隊。
  • 服務器使用 Default 同步上下文
  • 客戶端回調使用相應的 UI 同步上下文
  • 在需要重入時,這會導致問題,如客戶端調用的服務器方法回調客戶端方法。在這類情況下,將 UseSynchronizationContext 設置為 false 可以禁止 WCF 自動使用 同步上下文
    • 因為如果這時如果客戶端使用的是UI同步上下文,可能造成不可預期的問題

7.2. Workflow Foundation (WF)

  • WorkflowInstance 類及其派生的 WorkflowApplication 類的SynchronizationContext 屬性

  • 如果承載進程創建自己擁有的 WorkflowInstance ,同步上下文也許直接設置了

  • WorkflowInvoker.InvokeAsync 也使用 同步上下文

    • 它捕獲當前 同步上下文 並將其傳遞給其 internalWorkflowApplication
      • 該 同步上下文 用於 Post 工作流完成事件以及工作流活動

7.3. Task Parallel Library (TPL)

TaskScheduler.FromCurrentSynchronizationContext

TPL 使用 Task 對象作為其工作單元並通過 TaskScheduler 執行。

  • 默認 TaskScheduler 的作用類似於 Defalut 同步上下文 ,將 TaskThreadPool 中列隊。
  • TPL 隊列還提供了另一個 TaskScheduler ,將 Task 在 一個同步上下文 中列隊
    • UI 進度條更新 可以在一個嵌套 Task 中完成,如下所示。

UI 進度條更新


private void button1_Click(object sender, EventArgs e)
{
  // 捕獲當前 SynchronizationContext 的 TaskScheduler.
  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.
  });
}

CancellationToken.Register

  • CancellationToken 類可用於任意類型的取消操作
  • 為了與現有取消操作形式集成,該類允許注冊委托以在請求取消時調用
    • 當取消委托被注冊后,同步上下文就可以傳遞了
      • 當發起取消請求時, CancellationToken 將該委托列入 同步上下文 隊列,然后才會進行執行

7.4. Reactive Extensions (Rx)

ObserveOnSubscribeOnSynchronizationContextScheduler

Rx 是一個庫,它將事件視為數據流

  • ObserveOn(context) 運算符通過一個 同步上下文 將事件列隊
  • SubscribeOn(context) 運算符通過一個 同步上下文 將對這些事件的訂閱 列隊
  • ObserveOn(context) 通常用於使用傳入事件更新 UI,SubscribeOn 用於從 UI 對象使用事件

Rx 還有它自己的工作單元列隊方法: IScheduler 接口。

  • Rx 包含 SynchronizationContextScheduler
    • 是一個將 Task 列入指定 同步上下文 的 IScheduler 實現。
    • 構造方法: SynchronizationContextScheduler(SynchronizationContext context)

7.5. 異步編程 Async

awaitConfigureAwaitSwitchToProgress<T>

  • 默認情況下, 當前同步上下文 在一個 await 關鍵字處被捕獲
  • 同步上下文 用於在運行到 await關鍵字后時恢復
    • 也就是 await 關鍵字后面的執行代碼會被列入到 該同步上下文 中執行
      • 僅當它不為 null 時,才捕獲當前 同步上下文
      • 如果為 null ,則捕獲當前 TaskScheduler
private async void button1_Click(object sender, EventArgs e)
{
  // 當前 SynchronizationContext 被 await 在暗中捕獲
  var data = await webClient.DownloadStringTaskAsync(uri);

  // 此時,已捕獲的SynchronizationContext用於恢復執行,
  // 因此我們可以自由更新UI對象。
}
  • ConfigureAwait 提供了一種途徑避免 SynchronizationContext 捕獲;

    • continueOnCapturedContext 參數傳遞 false 會阻止 await后的代碼,在 await 執行前的 同步上下文 上執行
  • 同步上下文實例還有一種擴展方法 SwitchTo

    • 使用該方法,任何 async 的方法 可以通過調用 SwitchTo 改變到一個不同的同步上下文上,並 awaiting 結果

報告異步操作進展的通用模式:

  • IProgress<T> 接口及其實現 Progress<T>
    • 該類在構造時捕獲 當前同步上下文
    • 並在中引發其 ProgressChanged 事件
    • 所以實例化時,需要在 UI同步上下文 上執行

返回 voidasync 方法

  • 在異步操作開始時遞增計數
  • 在異步操作結束后遞減計數

這一行為使返回 voidasync 方法 類似於頂級異步操作。

8. 限制和功能

  • 了解 同步上下文 對任何編程人員來說都是有益的
  • 現有跨框架組件使用它同步其事件
  • Libraries 可以將它公開以獲得更高的靈活性
  • 技術精湛的編程人員了解 同步上下文 限制和功能后,可以更好地編寫和利用這些類


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM