SynchronizationContext(同步上下文)綜述

>>返回《C# 併發編程》html

1. 概述

不管是什麼平臺(ASP.NET 、WinForm 、WPF 等),全部 .NET 程序都包含 同步上下文 概念,而且全部多線程編程人員均可以經過理解和應用它獲益。react

2. 同步上下文 的必要性

2.1. ISynchronizeInvoke 的誕生

  • 原始多線程web

    • 多線程程序在 .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 能夠將它公開以得到更高的靈活性
  • 技術精湛的編程人員瞭解 同步上下文 限制和功能後,能夠更好地編寫和利用這些類
相關文章
相關標籤/搜索