>>回傳《C# 并發編程》
- 1. 概述
- 2. 同步背景關系 的必要性
- 2.1. ISynchronizeInvoke 的誕生
- 2.2. SynchronizationContext 的誕生
- 3. 同步背景關系 的概念
- 4. 同步背景關系 的實作
- 4.1. WinForm 同步背景關系
- 4.2. Dispatcher 同步背景關系
- 4.3. Default 同步背景關系
- 4.4. 背景關系捕獲和執行
- 4.5. AspNetSynchronizationContext
- 5. 同步上下實作類 的注意事項
- 6. AsyncOperationManager 和 AsyncOperation
- 7. 同步背景關系 的Library支持示例
- 7.1. WCF
- 7.2. Workflow Foundation (WF)
- 7.3. Task Parallel Library (TPL)
- 7.4. Reactive Extensions (Rx)
- 7.5. 異步編程 Async
- 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 = https://www.cnblogs.com/BigBrotherStone/p/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 可以將它公開以獲得更高的靈活性
- 技術精湛的編程人員了解 同步背景關系 限制和功能后,可以更好地撰寫和利用這些類
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/77411.html
標籤:C#
上一篇:C# 7
