- 1. 概述
- 2. 同步上下文 的必要性
- 3. 同步上下文 的概念
- 4. 同步上下文 的實現
- 5. 同步上下實現類 的注意事項
- 6. AsyncOperationManager 和 AsyncOperation
- 7. 同步上下文 的Library支持示例
- 8. 限制和功能
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 異步頁面不與單個線程關聯。 - 需要設計出,無須將工作排入原來的線程隊列,異步頁面只需對未完成的操作進行計數 以確定頁面請求何時可以完成。
- 使用
- .NET Framework 2.0 版包含很多重大改動。 其中一項重要改進是在 ASP.NET 體系結構中引入了異步頁面。
- 經過精心設計,
SynchronizationContext
取代了ISynchronizeInvoke
。
3. 同步上下文 的概念
ISynchronizeInvoke
滿足了兩點需求:
- 確定是否必須同步
- 使工作單元從一個線程列隊等候另一個線程。
設計 SynchronizationContext
是為了替代 ISynchronizeInvoke
,但完成設計后,它就不僅僅是一個替代品了。
- 一方面,
SynchronizationContext
提供了一種方式,可以使工作單元列隊並列入上下文。- 請注意,工作單元是列入上下文,而不是某個特定線程。
- 這一區別非常重要,因為很多
SynchronizationContext
實現都不是基於單個特定線程的。 SynchronizationContext
不包含用來確定是否必須同步的機制,因為這是不可能的。- WPF 中的
Dispatcher.Invoke
是將委托列入上下文,不等委托執行直接返回 - WinForm 中的
txtUName.Invoke
會啟動一個process,等到委托執行完畢后返回
- WPF 中的
- 另一方面,每個線程都有當前同步上下文。
- 線程上下文不一定唯一;
- 其上下文實例可以與多個其他線程共享。
- 線程可以更改其當前上下文,但這樣的情況非常少見。
- 第三個方面,保持了未完成操作的計數。
- 這樣,就可以用於 ASP.NET 異步頁面和需要此類計數的任何其他主機。
- 大多數情況下,捕獲到當前 SynchronizationContext 時,計數遞增;
- 捕獲到的 SynchronizationContext 用於將完成通知列隊到上下文中時,計數遞減
void OperationCompleted()
。
- 捕獲到的 SynchronizationContext 用於將完成通知列隊到上下文中時,計數遞減
- 其他一些方面,這些對大多數編程人員來說並不那么重要。
// 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 Control
的Invoke
等方法(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
,因此 RunWorkerCompleted
在 RunWorkerAsync
捕獲的 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
提供了一種途徑,可以在很多不同框架中編寫組件BackgroundWorker
和WebClient
就是兩個在WinForm
、WPF
、Console
和ASP.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
AsyncOperationManager
和AsyncOperation
類是SynchronizationContext
抽象類的輕型包裝AsyncOperation
的異步是使用抽象的同步上下文進行封裝的
AsyncOperationManager
在第一次創建AsyncOperation
時捕獲當前同步上下文 ,如果當前同步上下文為null
,則使用Default
同步上下文AsyncOperation
將委托異步發布到捕獲的 同步上下文- 大多數基於事件的異步組件都在其實現中使用
AsyncOperationManager
和AsyncOperation
- 這些對於具有明確完成點的異步操作非常有效
- 即異步操作從一個點開始,以另一個點的事件結束
- 其他異步通知可能沒有明確的完成點;它們可能是一種訂閱類型,在一個點開始,然后無限期持續
- 對於這些類型的操作,當觸發了被訂閱的事件,在事件處理中直接捕獲和使用同步上下文
- 這些對於具有明確完成點的異步操作非常有效
新組件不應使用基於事件的異步模式
- 使用基於Task的異步模式
- 組件返回 Task 和 Task
對象,而不是通過 同步上下文 引發事件 - 基於
Task
的 API 是 .NET 中異步編程的發展方向
- 組件返回 Task 和 Task
7. 同步上下文 的Library支持示例
- 像
BackgroundWorker
和WebClient
這樣的簡單組件是隱式自帶的- 隱藏了對同步上下文的捕獲和使用。
- 很多 Libraries 以更可見的方式使用 同步上下文
- 通過使用
SynchronizationContext
公開 API,Libraries 不僅獲得了框架獨立性,而且為高級最終用戶提供了一個可擴展點。
- 通過使用
ExecutionContext
- 是與執行的邏輯線程相關的所有信息提供單個容器。 這包括安全上下文、調用上下文和同步上下文
- 任何捕獲線程的 ExecutionContext 的系統都會捕獲當前 同步上下文
- 當恢復 ExecutionContext 時,通常也會恢復 同步上下文
7.1. WCF
WCF 有兩個用於配置服務器和客戶端行為的特性:
ServiceBehaviorAttribute
和CallbackBehaviorAttribute
- 這兩個特性都有一個 Boolean 屬性:UseSynchronizationContext
- 此特性的默認值為 true,這表示在創建通信通道時捕獲當前 同步上下文 ,這一捕獲的 同步上下文 用於使約定方法列隊。
- 服務器使用 Default 同步上下文
- 客戶端回調使用相應的 UI 同步上下文
- 在需要重入時,這會導致問題,如客戶端調用的服務器方法回調客戶端方法。在這類情況下,將
UseSynchronizationContext
設置為false
可以禁止 WCF 自動使用 同步上下文- 因為如果這時如果客戶端使用的是UI同步上下文,可能造成不可預期的問題
7.2. Workflow Foundation (WF)
-
WorkflowInstance
類及其派生的WorkflowApplication
類的SynchronizationContext
屬性 -
如果承載進程創建自己擁有的
WorkflowInstance
,同步上下文也許直接設置了 -
WorkflowInvoker.InvokeAsync
也使用 同步上下文- 它捕獲當前 同步上下文 並將其傳遞給其
internal
的WorkflowApplication
- 該 同步上下文 用於
Post
工作流完成事件以及工作流活動
- 該 同步上下文 用於
- 它捕獲當前 同步上下文 並將其傳遞給其
7.3. Task Parallel Library (TPL)
TaskScheduler.FromCurrentSynchronizationContext
TPL 使用 Task
對象作為其工作單元並通過 TaskScheduler
執行。
- 默認
TaskScheduler
的作用類似於 Defalut 同步上下文 ,將Task
在ThreadPool
中列隊。 - TPL 隊列還提供了另一個
TaskScheduler
,將Task
在 一個同步上下文 中列隊- UI 進度條更新 可以在一個嵌套
Task
中完成,如下所示。
- UI 進度條更新 可以在一個嵌套
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)
ObserveOn
、 SubscribeOn
和 SynchronizationContextScheduler
Rx 是一個庫,它將事件視為數據流
ObserveOn(context)
運算符通過一個 同步上下文 將事件列隊SubscribeOn(context)
運算符通過一個 同步上下文 將對這些事件的訂閱 列隊ObserveOn(context)
通常用於使用傳入事件更新 UI,SubscribeOn 用於從 UI 對象使用事件
Rx 還有它自己的工作單元列隊方法: IScheduler
接口。
- Rx 包含
SynchronizationContextScheduler
- 是一個將 Task 列入指定 同步上下文 的 IScheduler 實現。
- 構造方法:
SynchronizationContextScheduler(SynchronizationContext context)
7.5. 異步編程 Async
await
、 ConfigureAwait
、 SwitchTo
和 Progress<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同步上下文 上執行
返回 void
的 async
方法
- 在異步操作開始時遞增計數
- 在異步操作結束后遞減計數
這一行為使返回 void
的 async
方法 類似於頂級異步操作。
8. 限制和功能
- 了解 同步上下文 對任何編程人員來說都是有益的
- 現有跨框架組件使用它同步其事件
- Libraries 可以將它公開以獲得更高的靈活性
- 技術精湛的編程人員了解 同步上下文 限制和功能后,可以更好地編寫和利用這些類