ExecutionContext(執行上下文)綜述

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

1. 簡介

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

下面開始本次講述:安全

ExecutionContext 實際上只是線程相關其餘上下文的容器。架構

  • 有些上下文起輔助做用
  • 有些上下文對 .Net 執行模型相當重要

ExecutionContext周圍環境的信息有關,這意味着,代碼正在運行時,它存儲了與 當前環境 或 「context」 有關的數據。併發

周圍環境: 代碼執行處,能夠訪問到的變量、方法、屬性等等。框架

2. 同步異步對比

同步世界異步

  • 在許多系統中,此類「周圍」的信息在線程本地存儲(TLS)中維護,例如在 [ThreadStatic] 字段或 ThreadLocal<T> 中。
    • 同步世界中,這樣的 thread-local 信息就足夠了。
      • 任何事情發生在該線程上,也就是無論在該線程上所處的堆棧結構是什麼,正在執行什麼方法,等等。
      • 全部在該線程上運行的代碼均可以查看影響該線程特有的數據。

異步世界,TLS變得可有可無,同步異步對比:async

  • 同步
    • 例如:
      • 若是我先執行操做 A
      • 而後執行操做 B
      • 而後執行操做 C
    • 則全部這三個操做都在同一線程上發生
    • 所以全部這三個操做都受該線程上存儲的周圍環境數據的影響。
  • 異步
    • 例如:
      • 我可能在一個線程上啓動 A
      • 而後在另外一個線程上完成它
        • 這樣操做 B 能夠在不一樣於 A 的線程上啓動或運行
        • 而且相似地使 C 能夠在不一樣於 B 的線程上啓動或運行。
    • 這意味着咱們用來控制執行細節的周圍環境context再也不可行,由於TLS不會「流」過這些異步點。
    • Thread-local 存儲特定於線程,這些異步操做並不與特定線程相關聯。
    • 可是,一般存在邏輯控制流,咱們但願這些周圍環境的數據與該控制流一塊兒流動,以使周圍環境的數據從一個線程移動到另外一個線程
    • 這就 須要 ExecutionContext 來完成這些操做。

3. 上下文的捕獲和恢復

ExecutionContext 其實是一個 state 包post

  • 用於從一個線程上捕獲全部 state
  • 而後在控制邏輯流的同時將其還原到另外一個線程

ExecutionContext 是使用靜態方法 Capture 捕獲的:優化

// 周圍環境的 state 捕獲到 ec 中
ExecutionContext ec = ExecutionContext.Capture();

經過靜態方法 Run ,在委託(Run方法的參數)調用時恢復 ExecutionContext

ExecutionContext.Run(ec, delegate
{
    … // 這裏的代碼將上述 ec 的狀態視爲周圍環境
}, null);

全部派生異步工做的方法都以這種方式捕獲還原 ExecutionContext 的。

  • 帶有「Unsafe」字樣的方法除外,它們是不安全的,由於它們不傳播 ExecutionContext

例如:

  • 當您使用 Task.Run 時,對 Run 的調用將從調用線程捕獲 ExecutionContext ,並將該 ExecutionContext 實例存儲到 Task 對象中
  • 當提供給 Task.Run 的委託做爲該 Task 執行的一部分被調用時,它是使用存儲的 ExecutionContext 經過 ExecutionContext.Run 來完成的

如下全部異步API的執行都是捕獲 ExecutionContext 並將其存儲,而後在調用某些代碼時再使用存儲的 ExecutionContext

  • Task.Run
  • ThreadPool.QueueUserWorkItem
  • Delegate.BeginInvoke
  • Stream.BeginRead
  • DispatcherSynchronizationContext.Post
  • 任何其餘異步API

當咱們談論「flowing ExecutionContext」時,咱們其實是在討論:

  • 在一個線程上獲取周圍環境狀態
  • 在稍後的某個時刻將該狀態恢復到另外一個線程上(須要執行提供的委託的線程)。

4. Flowing ExecutionContext vs Using SynchronizationContext

前面咱們介紹了 SynchronizationContext 是如何調度線程的,如今,咱們要進行進行一次對比:

  • flowing ExecutionContext 在語義上與 capturing and posting to a SynchronizationContext 徹底不一樣。
  • ExecutionContext 流動時,您是從一個線程捕獲 state ,而後還原該 state
    • 使提供的委託執行時處於周圍環境 state
  • 當您捕獲使用 SynchronizationContext 時,不會發生這種狀況。
    • 捕獲部分是相同的,由於您要從當前線程中獲取數據,可是隨後用不一樣方式使用 state
    • SynchronizationContext.Post 只是使用捕獲的狀態來調用委託,而不是在調用委託時設置該狀態爲當前狀態
      • 委託在什麼時候何地以及如何運行徹底取決Post方法的實現

5. 如何適用於 async/await

asyncawait 關鍵字背後的框架支持會自動與 ExecutionContextSynchronizationContext 交互。

每當代碼等待一個可等待項(awaitable),該可等待項(awaitable)等待者(awaiter) 說還沒有完成時

  • 等待者(awaiter)IsCompleted 返回 false

則該方法須要暫停,並經過等待者(awaiter)continuation 來恢復。

等待者(awaiter) : 能夠理解爲 await 產生的 Task對象

5.1. 實現方式

5.1.1. ExecutionContext

  • 前面已經提到過了, ExecutionContext 須要從發出 await 的代碼一直流到 continuation 委託的執行。
    • 這是由框架自動處理的
    • async 方法即將掛起時,基礎設施將捕獲 ExecutionContext
    • 獲得的委託交給等待者(awaiter) ,並且此等待者(awaiter) 具備對此 ExecutionContext 實例的引用,並將在恢復該方法時使用它。
  • ExecutionContext 帶領,啓用重要的周圍環境信息,去流過 awaits

5.1.2. SynchronizationContext

該框架還支持 SynchronizationContext 。前述對 ExecutionContext 的支持內置於表示 async 方法的「構建器」中

  • 例如 System.Runtime.CompilerServices.AsyncTaskMethodBuilder
  • await / async 會被編譯成執行碼

而且這些構建器可確保 ExecutionContextawait 點流動,不管使用哪一種可等待項(awaitable)

相反,對 SynchronizationContext 的支持內置在 awaiting 的且已經構建好的TaskTask<TResult>

自定義的等待者(awaiter) (好比 new Task(...))能夠本身添加相似的邏輯,可是不會自動得到實例化時的SynchronizationContext

  • 這是設計使然,由於可以自定義什麼時候以及如何調用 continuation 是自定義Task有用的一部分緣由。

5.2. 執行過程

5.2.1. SynchronizationContext 使用和控制

  • 當您 await 一個 task 時,默認狀況下,等待者(awaiter) 將捕獲當前的 SynchronizationContext(若是有的話)
  • task 完成時將 Post 這個前面提供的 continuation 委託並回到該 context 進行執行
    • 運行委託的:不是在完成了 task 的線程上,也不是在 ThreadPool 的線程上

若是開發人員不但願這種封送處理行爲,則能夠經過更改在那裏使用的 可等待項(awaitable) / 等待者(awaiter) 來控制它。

  • 大多數狀況,等待 TaskTask<TResult> 就時採用上述方式
  • 能夠經過 await 方法 task.ConfigureAwait(…)的返回值來修改這種封送處理行爲
    • ConfigureAwait() 返回一個 可等待項(awaitable),它能夠抑制此默認的封送處理行爲。
    • ConfigureAwait() 的惟一 bool 類型參數 continueOnCapturedContext
      • true ,那麼將得到默認行爲;
      • false ,則等待者(awaiter) 不檢查 SynchronizationContext ,就像沒有同樣
    • 注意: 當等待的任務完成時,不管 ConfigureAwait 如何,在恢復執行的線程上,運行時都會檢查當前的 context ,以肯定:
      • continuation 是否能夠在此處同步運行
      • continuation 是否必須今後處開始異步調度(scheduled asynchronously)

5.2.2. ExecutionContext 的流動沒法控制

儘管 ConfigureAwait 提供了,用於改變 SynchronizationContext 行爲的、顯示的、與 await 相關的編程模型,可是沒有用於抑制 ExecutionContext 流動的、與 await 相關的編程模型支持。

  • 這是故意的
  • 開發人員在編寫異步代碼時沒必要擔憂 ExecutionContext
  • 它在基礎架構級別上的支持,有助於在異步環境中模擬同步方式的語義(即TLS);

6. 二者的關係

7. 說明

SynchronizationContext 不是 ExecutionContext 的一部分嗎?

  • ExecutionContext 可以帶着全部的上下文(例如 SecurityContextHostExecutionContextCallContext 等)流動
    • 確實也包括 SynchronizationContext
  • 我我的認爲,這是API設計的一個錯誤,自從它在許多版本的.NET中提出以來,就引發了一些問題
  • 注意這個問題在 .Net Core 已經解決
    • .Net Core 中的 ExecutionContext 已不包含任何其餘 context

當您調用公共 ExecutionContext.Capture() 方法時,它將檢查當前的 SynchronizationContext ,若是有,則將其存儲到返回的 ExecutionContext 實例中。而後,當使用公共 ExecutionContext.Run(...) 方法時,在提供的委託執行期間,該捕獲的 SynchronizationContext 被恢復爲 Current

爲何這有問題?做爲 ExecutionContext 的一部分而流動的 SynchronizationContext 更改了 SynchronizationContext.Current 的含義。

應該能夠經過 SynchronizationContext.Current 返回到你最近調用 Current 時的環境

  • 所以,若是 SynchronizationContext 流出,成爲另外一個線程的當前 SynchronizationContext ,則 SynchronizationContext.Current 就沒有意義了,因此不是這樣設計的

7.1. 示例

解釋此問題的一個示例,代碼以下:

private async void button1_Click(object sender, EventArgs e)
{
    button1.Text = await Task.Run(async delegate
    {
        string data = await DownloadAsync();
        return Compute(data);
    });
}

7.1.1. 運行過程解析

  • 用戶單擊 button1 ,致使UI框架在UI線程上調用 button1_Click 事件;
  • 而後,代碼啓動一個 WorkItemThreadPool 上運行(經過Task.Run);
    • WorkItemThreadPool介紹-異步調用方法 中提到;
    • 這個 WorkItem 開始一些下載工做,並異步等待其完成;
    • 在下載完成以後,ThreadPool 上的 WorkItem 進行一些密集型操做(Compute(data));
    • 返回結果
  • WorkItem 執行完成後,致使正在 UI線程等待Task 完成
  • (下載獲得結果,返回結果),成爲 UI線程 等待完成的 ;
  • 而後,UI線程 處理 button1_Click 方法的剩餘部分: 保存計算結果到 button1.Text 屬性。

7.1.2. 帶來的思考

若是 SynchronizationContext 不做爲 ExecutionContext 的一部分流動,個人預期就是有根據的。

若是 SynchronizationContext 流動了,不管如何,我將感到很是失望。

假設SynchronizationContext 做爲 ExecutionContext 的一部分流動:

  • Task.Run 在調用時捕獲 ExecutionContext ,並使用它運行傳遞給它委託。
  • 這就意味着 Task.Run 調用時的當前 SynchronizationContext 將流動到 Task 中,並且將在 DownloadAsync 執行和等待結果期間成爲當前 SynchronizationContext
    • 這意味着這個 await 將看到當前 SynchronizationContext ,並 Post 異步方法的其他部分做爲一個 continuation 返回到 UI線程 上運行。
  • 這意味着個人 Compute 方法將在 UI線程 上運行,而不是在 ThreadPool 上運行,從而致使個人應用程序出現響應性問題。
  • 從實際結果來看這是不對的,假設執行的代碼更像下面的

    private async void button1_Click(object sender, EventArgs e)
    {
      string data = await DownloadAsync();
      button1.Text = Compute(data);
    }

實際: 如今,咱們看看實際是如何處理的:

Task.Run(...) 這種異步Api的實現

  • 解讀捕獲(Capture)和運行(Run);
    • ExecutionContext 實際上有兩個 Capture 方法:
      • 可是隻有一個是 public,供外部使用
      • 那個 internal 的方法,是 mscorlib 大多數公開的異步功能(如:Task.Run(...))所使用的一個
        • 這個方法有選擇地容許調用方抑制捕獲 SynchronizationContext 做爲 ExecutionContext 的一部分;
    • 與此相對應的是, Run 方法的 internal 重載也支持忽略存儲在 ExecutionContext 中的 SynchronizationContext
      • 其實是僞裝沒有被捕獲(此外,這mscorlib 中大多數方法使用的重載)。
  • 這意味着:

    • mscorlib 中幾乎包含全部異步操做的核心實現,這裏不會將 SynchronizationContext 做爲 ExecutionContext 的一部分流動
    • 位於其餘地方的,任何異步操做的核心實現,都將使 SynchronizationContext 做爲 ExecutionContext 的一部分流動。

標識 async 關鍵字方法的實現:

  • 以前我曾提到,異步方法的 「builders」 是負責在 async 方法中流動 ExecutionContext 所使用的方式
    • 這些 builders 確實存在於 mscorlib 中,而且確實使用 internal 的重載作一些事情。
  • 一樣的, SynchronizationContext 不會做爲 ExecutionContext 的一部分流動穿過 awaits
    • 此外,這與 task awaiters 如何支持 捕獲 SynchronizationContext 和將其 Post 回來是分開的
    • 實現方式: 爲了幫助處理 ExecutionContext 帶着 SynchronizationContext 流動的狀況, async 方法的基礎設施嘗試忽略因爲流動而將 SynchronizationContexts 設置爲 Current
  • 簡而言之,SynchronizationContext.Current 不會「流動」穿過 await 點。

參考資料
《ExecutionContext vs SynchronizationContext》 --- Stephen Toub

相關文章
相關標籤/搜索