【C# TAP 異步編程】四、SynchronizationContext 同步上下文|ExecutionContext


 

 

一、同步上下文(SynchronizationContext)概述

 

由來

多線程程序在.net框架出現之前就已經存在了。這些程序通常需要一個線程將一個工作單元傳遞給另一個線程。Windows程序以消息循環為中心,因此許多程序員使用這個內置隊列來傳遞工作單元。每個想要以這種方式使用Windows消息隊列的多線程程序都必須定義自己的自定義Windows消息和處理它的約定。

當.net框架首次發布時,這種通用模式被標准化了。那時,. net支持的唯一GUI應用程序類型是Windows窗體。然而,框架設計者預期了其他模型,他們開發了一個通用的解決方案。ISynchronizeInvoke誕生了。而SynchronizationContext就是用來取代ISynchronizeInvoke。

1、概念

同步上下文:這里同步是動詞,據有歸類的功能,假如有A(UI線程)、B類兩線程,B線程要更新A線程的內容。如果直接在B中更新A中內容,那就是B線程多管閑事了,增加程序的耦合。為了降低程序的耦合度,B線程必須把更新A線程UI的事情還給A線程, 以消息方式把

要更改內容發送給A線程,A線程有一個堆棧用來保存B線程發送過來的消息。然后A線程根據自己情況決定什么時候更新。

如果B線程以Send()方法給A線程發送消息,B線程發送消息后什么事情都不做一直等待A線程的回復(同步),對應SynchronizationContext.Send()方法。

如果B線程以Post()方法給A線程發送消息,B線程發送完消息后就去做其他事情了(異步)),對應SynchronizationContext.Post()方法(asp.net 除外)。

同步上下文是一種可以將工作單元(執行某些方法 多播委托)排隊到上下文(主要是不同的線程)的方法。
它的作用通俗來講就是實現線程之間通訊的。

同步上下文應用於很多場景,比如在WinForms和WPF中,只有一個UI線程可以更新UI元素(文本框,復選框等)。如果嘗試從另一個非UI線程更改文本框的內容,則不會發生更改,也可能拋出異常(取決於UI框架)。因此,在這樣的應用程序中,非UI線程需要將對UI元素的所有更改安排到UI線程。這就是同步上下文提供的內容。它允許將一個工作單元(執行某些方法)發布到不同的上下文 - 在這種情況下是UI線程。

 注意:cpu每30毫秒切換一次

2、作用域

無論是什么平台(ASP.NET、Windows 窗體、Windows Presentation Foundation (WPF)、Silverlight 或其他),所有 .NET 程序都包含 SynchronizationContext 概念。

Microsoft .NET Framework提供了同步上下文的SynchronizationContext類。根據平台框架不同,又單獨提供了WindowsFormsSynchronizationContext(WinForm)類、DispatcherSynchronizationContext(WPF)類等同步上下文的模型但都是繼承自SynchronizationContext類。

每個線程都有一個默認的SynchronizationContext,但是不是每個線程都附加SynchronizationContext.Current這個對象,只有UI線程是一直擁有的SynchronizationContext.Current。故獲取SynchronizationContext.Current也只能在UI線程上進行SynchronizationContext context = SynchronizationContext.Current;

 

//只有UI線程能獲取到 值,這是創建一個副本,不同ExecutionContent.capture, SynchronizationContext maincontent= SynchronizationContext.Current;//這是創建一個副本。ExecutionContent.capture是捕獲引用 //asp.net 和控制獲取的結果是null SynchronizationContext maincontent= SynchronizationContext.Current;

3、原理

通過UI線程與工作線程的時序圖可以看出整個更新的步驟:

 

 整個過程中關鍵的是主線程的SynchronizationContext,SynchronizationContext在通訊中充當傳輸者的角色。在線程執行過程中,需要更新到UI控件上的數據不再直接更新,而是通過UI線程上下文的Post/Send方法,將數據以異步/同步消息的形式發送到UI線程的消息隊列;UI線程收到該消息后,根據消息是異步消息還是同步消息來決定通過異步/同步的方式調用SetTextSafePost方法直接更新自己的控件了。在本質上,向UI線程發送的消息並是不簡單數據,而是一條委托調用命令。

 

4、作用:傳輸者

傳輸者SynchronizationContext在通訊中充當傳輸者的角色(用Post/Send方法實現傳輸),實現功能就是一個線程和另外一個線程的通訊。SynchronizationContext將UI線程的同步環境保存下來,讓這個環境可以在不同的線程之間流動,其他非UI線程可以用這個環境回到要ui線程執行的任務。例如在winform應用中,非UI線程中利用這個環境更新UI控件的內容,而不是利用控件的invoke方法。 SynchronizationContext.post()表示啟用一個新線程來執行委托(異步執行)。SynchronizationContext.send()表示在當前線程執行(同步的)。SynchronizationContext.post是同步上下文最重要的一個方法。

Send:發送界面更新請求至主線程,阻塞當前線程直至返回。SynchronizationContext.Send(SendOrPostCallback d,object state): 
Post:發送界面更新請求至主線程,不阻塞當前線程。SynchronizationContext.Post(SendOrPostCallback d,object state);
public delegate void SendOrPostCallback(object state);d 為一個沒有返回值,並且具有一個Object類型傳入參數的委托(SendOrPostCallback );state 為執行這個委托時的參數(object);

注意:
  SynchronizationContext的對象不是所有線程都被附加的,只有UI主線程會被附加。
  對於UI線程來說,是如何將SynchronizationContext這個對象附加到線程上的呢?
  在Form1 form = new Form1()之前,SynchronizationContext對象是為空,而當實例化Form1窗體后,SynchronizationContext對象就被附加到這個線程上了。
  所以可以得出答案了:當Control對象被創建的同時,SynchronizationContext對象也會被創建並附加到線程上。所以在使用時,一定要等窗體InitializeComponent(); 這個完成后 它才能得到一個不是NULL的對象.

5、應用:那么如何編寫我的組件與UI框架無關呢?

看過很多介紹文章介紹如何在后台線程更新主界面的,多數都是使用Control.Invoke, Control.BeginInvoke。這些都是很好的解決方案,不過有兩個問題:
1. 必須的引用System.Windows.Forms,然后 using System.Windows.Forms
2. 代碼結構比較零亂。(實際上這個也是#1 導致的)
微軟提供了另外一個比較優雅的解決方案,就是 System.Threading. SynchronizationContext。 可以看出,它不在 namesapce System.Windows.Forms 里面,因此我們可以理直氣壯地用在BusinessLaryer, Controler,甚至 module 里面。

其實在UI線程中使用的並不是SynchronizationContext這個類,而是WindowsFormsSynchronizationContext這個東東,它是SynchronizationContext的派生類。

以下是SynchronizationContext在winform中實際應用:

案例一

View Code

  案例二

View Code

 

 

 二、同步上下文的派生

同步上下文的實際"上下文"沒有明確定義。不同的框架和主機可以自由地定義自己的上下文。了解這些不同的實現及其局限性,可以准確地闡明同步上下文概念的作用和不足之處。我將簡要討論其中的一些實現。

1、WinForm 的同步上下文

WindowsFormsSynchronizationContext繼承自SynchronizationContext
命名空間:System.Windows.Forms.dll:System.Windows.Forms

實現:

Windows Forms應用程序將創建WindowsFormsSynchronizationContext並將其安裝為創建UI控件的任何線程的當前上下文。
此SynchronizationContext在UI控件上使用ISynchronizeInvoke方法,該控件將委托傳遞到基礎Win32消息循環。
WindowsFormsSynchronizationContext的上下文是單個UI線程(單線程的)。
排隊到WindowsFormsSynchronizationContext的所有委托一次執行一次;它們由特定的UI線程按排隊順序執行。當前實現為每個UI線程創建一個WindowsFormsSynchronizationContext。

為了避免死鎖,就要防止異步線程切換道ui線程,代碼要這樣寫:

async Task DoAsync()
{
    await Task.Run(() => { }).ConfigureAwait(false);
}

 

2、WPF框架中同步上下文 

源代碼

using System;
using System.Threading;
using System.Windows;
using System.Windows.Threading;
using MS.Win32;

public sealed class DispatcherSynchronizationContext : SynchronizationContext
{
    internal Dispatcher _dispatcher;

    private DispatcherPriority _priority;

    public DispatcherSynchronizationContext()
        : this(Dispatcher.CurrentDispatcher, DispatcherPriority.Normal)
    {
    }

    public DispatcherSynchronizationContext(Dispatcher dispatcher)
        : this(dispatcher, DispatcherPriority.Normal)
    {
    }

    public DispatcherSynchronizationContext(Dispatcher dispatcher, DispatcherPriority priority)
    {
        if (dispatcher == null)
        {
            throw new ArgumentNullException("dispatcher");
        }
        Dispatcher.ValidatePriority(priority, "priority");
        _dispatcher = dispatcher;
        _priority = priority;
        SetWaitNotificationRequired();
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        if (BaseCompatibilityPreferences.GetInlineDispatcherSynchronizationContextSend() && _dispatcher.CheckAccess())
        {
            _dispatcher.Invoke(DispatcherPriority.Send, d, state);
        }
        else
        {
            _dispatcher.Invoke(_priority, d, state);
        }
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        _dispatcher.BeginInvoke(_priority, d, state);
    }

    public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
    {
        if (_dispatcher._disableProcessingCount > 0)
        {
            return UnsafeNativeMethods.WaitForMultipleObjectsEx(waitHandles.Length, waitHandles, waitAll, millisecondsTimeout, false);
        }
        return SynchronizationContext.WaitHelper(waitHandles, waitAll, millisecondsTimeout);
    }

    public override SynchronizationContext CreateCopy()
    {
        if (BaseCompatibilityPreferences.GetReuseDispatcherSynchronizationContextInstance())
        {
            return this;
        }
        if (BaseCompatibilityPreferences.GetFlowDispatcherSynchronizationContextPriority())
        {
            return new DispatcherSynchronizationContext(_dispatcher, _priority);
        }
        return new DispatcherSynchronizationContext(_dispatcher, DispatcherPriority.Normal);
    }
}
View Code

 

DispatcherSynchronizationContext繼承自SynchronizationContext
命名空間:WindowsBase.dll:System.Windows.Threading

實現:

Dispatcher的作用是用於管理線程工作項隊列,類似於Win32中的消息隊列,Dispatcher的內部函數,仍然調用了傳統的創建窗口類,創建窗口,建立消息泵等操作。
WPF和Silverlight應用程序使用DispatcherSynchronizationContext,該代理將對UI線程的Dispatcher的委托以“Normal”優先級排隊。
當線程通過調用Dispatcher.Run開始循環調度器 ,將這個初始化完成的 同步上下文 安裝到當前上下文。
DispatcherSynchronizationContext的上下文是單個UI線程(單線程的
排隊到DispatcherSynchronizationContext的所有委托均由特定的UI線程一次按其排隊的順序執行。當前實現為每個頂級窗口創建一個DispatcherSynchronizationContext,即使它們都共享相同的基礎Dispatcher。

 

為了避免死鎖,就要防止異步線程切換道ui線程,代碼要這樣寫:

async Task DoAsync() { await Task.Run(() => { }).ConfigureAwait(false); }



 

3、默認同步上下文

源代碼

// System.Threading.SynchronizationContext
using System;
using System.Runtime.CompilerServices;
using System.Threading;

[NullableContext(1)]
[Nullable(0)]
public class SynchronizationContext
{
    [Serializable]
    [CompilerGenerated]
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();

        [TupleElementNames(new string[] { "d", "state" })]
        public static Action<ValueTuple<SendOrPostCallback, object>> <>9__8_0;

        internal void <Post>b__8_0([TupleElementNames(new string[] { "d", "state" })] ValueTuple<SendOrPostCallback, object> s)
        {
            s.Item1(s.Item2);
        }
    }

    private bool _requireWaitNotification;

    [Nullable(2)]
    public static SynchronizationContext Current
    {
        [NullableContext(2)]
        get
        {
            return Thread.CurrentThread._synchronizationContext;
        }
    }

    private static int InvokeWaitMethodHelper(SynchronizationContext syncContext, IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
    {
        return syncContext.Wait(waitHandles, waitAll, millisecondsTimeout);
    }

    protected void SetWaitNotificationRequired()
    {
        _requireWaitNotification = true;
    }

    public bool IsWaitNotificationRequired()
    {
        return _requireWaitNotification;
    }

    public virtual void Send(SendOrPostCallback d, [Nullable(2)] object state)
    {
        d(state);
    }

    public virtual void Post(SendOrPostCallback d, [Nullable(2)] object state)
    {
        ThreadPool.QueueUserWorkItem(<>c.<>9__8_0 ?? (<>c.<>9__8_0 = new Action<ValueTuple<SendOrPostCallback, object>>(<>c.<>9.<Post>b__8_0)), new ValueTuple<SendOrPostCallback, object>(d, state), false);
    }

    public virtual void OperationStarted()
    {
    }

    public virtual void OperationCompleted()
    {
    }

    [CLSCompliant(false)]
    public virtual int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
    {
        return WaitHelper(waitHandles, waitAll, millisecondsTimeout);
    }

    [CLSCompliant(false)]
    protected static int WaitHelper(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
    {
        if (waitHandles == null)
        {
            throw new ArgumentNullException("waitHandles");
        }
        return WaitHandle.WaitMultipleIgnoringSyncContext(waitHandles, waitAll, millisecondsTimeout);
    }

    [NullableContext(2)]
    public static void SetSynchronizationContext(SynchronizationContext syncContext)
    {
        Thread.CurrentThread._synchronizationContext = syncContext;
    }

    public virtual SynchronizationContext CreateCopy()
    {
        return new SynchronizationContext();
    }
}
View Code

 

(默認)SynchronizationContext位於: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

 

默認SynchronizationContext是多線程的

 默認的同步上下文只是將任務安排到線程池操作隊列

public virtual void Post(SendOrPostCallback d, Object state)
{
   ThreadPool.QueueUserWorkItem(new WaitCallback(d), state);
}

public virtual void Send(SendOrPostCallback d, [Nullable(2)] object state)
    {
        d(state);
    }
public delegate void SendOrPostCallback(object state);

 

 設置同步上下文

using System;
using System.Threading;
using System.Threading.Tasks;

namespace GetSetContext
{
    internal static class Program
    {
        public static async Task Main(string[] args)
        {   
            Console.WriteLine($"Current Synchronization Context: {SynchronizationContext.Current}");
            
            SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext());
            
            Console.WriteLine($"Current Synchronization Context: {SynchronizationContext.Current}");
            
            await Task.Delay(100);
            
            Console.WriteLine("Completed!");
        }

        private class MySynchronizationContext : SynchronizationContext
        {
            public override void Post(SendOrPostCallback d, object state)
            {
                Console.WriteLine($"Continuation dispatched to {nameof(MySynchronizationContext)}");
                d.Invoke(state);
            }
        }
    }
}

這段代碼輸出以下內容:

Current Synchronization Context: null
Current Synchronization Context: MySynchronizationContext
Continuation dispatched to MySynchronizationContext
Completed!

上下文捕獲和執行

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 類可用作通用同步上下文實現。
 

二、ExecutionContext 上下文的捕獲和恢復

 

 三、ExecutionContext 概述

 

 

 

1、ExecutionContext 實際上只是其他上下文的容器。因此在.net framework中ExecutionContext 包含:同步上下文、安全上下文、調用上下文、模擬上下文、區域性通常會與執行上下文一起流動。

 

2、在.net core中, 不支持安全上下文和調用上下文、同步上下文和ExecutionContex一起流動。

1、2兩條內容來源:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.executioncontext?view=net-6.0

ExecutionContext 實際上是一個 state 包

  • 用於從一個線程上捕獲所有 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”時,我們實際上是在討論:

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

 

 ExecutionContext類創建副本以便傳播

  • ExecutionContext無法在另一個線程上設置與線程關聯的。 嘗試這樣做將導致引發異常。 若要將 ExecutionContext 從一個線程傳播到另一個線程,請創建的副本 ExecutionContext
  •  ExecutionContext類提供  CreateCopy 創建當前執行上下文的副本。

三.   ExecutionContext 和 SynchronizationContext使用區別

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

  • flowing ExecutionContext 在語義上與 capturing and posting to a SynchronizationContext 完全不同。
  • ExecutionContext 的流動無法控制、這是框架故意這樣設計的,開發人員在編寫異步代碼時不必擔心 ExecutionContext ;它在基礎架構級別上的支持,有助於在異步環境中模擬同步方式的語義(即TLS);

  • SynchronizationContext的流動是可以控制的,可以通過task.ConfigureAwait(bool) 傳入false關閉為 false ,則等待者(awaiter) 不檢查 SynchronizationContext ,就像沒有一樣

  • 當 ExecutionContext 流動時,您是從一個線程捕獲 state ,然后還原該 state
    • 使提供的委托執行時處於周圍環境 state
  • 當您捕獲使用 SynchronizationContext 時,不會發生這種情況。
    • 捕獲部分是相同的,因為您要從當前線程中獲取數據,但是隨后用不同方式使用 state
    • SynchronizationContext.Post 只是使用捕獲的狀態來調用委托,而不是在調用委托時設置該狀態為當前狀態
      • 委托在何時何地以及如何運行完全取決Post方法的實現
         
         
         
         

參考文章:

https://www.cnblogs.com/BigBrotherStone/p/12240731.html#%E4%B8%8A%E4%B8%8B%E6%96%87%E6%8D%95%E8%8E%B7%E5%92%8C%E6%89%A7%E8%A1%8C

https://blog.csdn.net/starrycraft/article/details/113658608

https://docs.microsoft.com/zh-cn/archive/msdn-magazine/2011/february/msdn-magazine-parallel-computing-it-s-all-about-the-synchronizationcontext

https://blog.csdn.net/kalvin_y_liu/article/details/117787437?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.nonecase


免責聲明!

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



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