從執行上下文角度重新理解.NET(Core)的多線程編程[2]:同步上下文


一般情況下,我們可以將某項操作分發給任意線程來執行,但有的操作確實對於執行的線程是有要求的,最為典型的場景就是:GUI針對UI元素的操作必須在UI主線程中執行。將指定的操作分發給指定線程進行執行的需求可以通過同步上下文(SynchronizationContext)來實現。你可能從來沒有使用過SynchronizationContext,但是在基於Task的異步編程中,它卻總是默默存在。今天我們就來認識一下這個SynchronizationContext對象。

目錄
一、從一個GUI的例子談起
二、自定義一個SynchronizationContext
三、ConfiguredTaskAwaitable方法
四、再次回到開篇的例子

一、從一個GUI的例子談起

GUI后台線程將UI操作分發給UI主線程進行執行時SynchronizationContext的一個非常典型的應用場景。以一個Windows Forms應用為例,我們按照如下的代碼注冊了窗體Form1的Load事件,事件處理器負責修改當前窗體的Text屬性。由於我們使用了線程池,所以針對UI元素的操作(設置窗體的Text屬性)將不會再UI主線程中執行。

partial class Form1
{  
    private void InitializeComponent()
    {
        ...
        this.Load += Form1_Load;
    }
    private void Form1_Load(object sender, EventArgs e)=>ThreadPool.QueueUserWorkItem(_ => Text = "Hello World");
}

當這個Windows Forms應用啟動之后,設置Form1的Text屬性的那行代碼將會拋出如下所示的InvalidOperationException異常,並提示“Cross-thread operation not valid: Control '' accessed from a thread other than the thread it was created on.”

image

我們可以按照如下的方式利用SynchronizationContext來解決這個問題。如代碼片段所示,在利用線程池執行異步操作之前,我們調用Current靜態屬性得到當前的SynchronizationContext。對於GUI應用來說,這個同步上下文將於UI線程綁定在一起,我們可以利用它將指定的操作分發給UI線程來執行。具體來說,針對UI線程的分發是通過調用其Post方法來完成的。

partial class Form1
{  
    private void InitializeComponent()
    {
        ...
        this.Load += Form1_Load;
    }
    private void Form1_Load(object sender, EventArgs e)
    {
        var syncContext = SynchronizationContext.Current;
        ThreadPool.QueueUserWorkItem(_ => syncContext.Post(_=>Text = "Hello World", null));
    }
}

二、自定義一個SynchronizationContext

雖然被命名為SynchronizationContext,並且很多場景下我們利用該對象旨在異步線程中同步執行部分操作的問題(比如上面這個例子),但原則上可以利用自定義的SynchronizationContext對分發給的操作進行100%的控制。在如下的代碼中,我們創建一個FixedThreadSynchronizationContext類型,它會使用一個單一固定的線程來執行分發給它的操作。FixedThreadSynchronizationContext繼承自SynchronizationContext,它將分發給它的操作(體現為一個SendOrPostCallback類型的委托)置於一個隊列中,並創建一個獨立的線程依次提取它們並執行。

public class FixedThreadSynchronizationContext:SynchronizationContext
{
    private readonly ConcurrentQueue<(SendOrPostCallback Callback, object State)> _workItems;
    public FixedThreadSynchronizationContext()
    {
        _workItems = new ConcurrentQueue<(SendOrPostCallback Callback, object State)>();
        var thread = new Thread(StartLoop);
        Console.WriteLine("FixedThreadSynchronizationContext.ThreadId:{0}", thread.ManagedThreadId);
        thread.Start();
        void StartLoop()
        {
            while (true)
            {
                if (_workItems.TryDequeue(out var workItem))
                {
                    workItem.Callback(workItem.State);
                }
            }
        }
    }
    public override void Post(SendOrPostCallback d, object state) => _workItems.Enqueue((d, state));
    public override void Send(SendOrPostCallback d, object state)=> throw new NotImplementedException();
}

向SynchronizationContext分發指定的操作可以調用Post和Send方法,它們之間差異就是異步和同步的差異。FixedThreadSynchronizationContext僅僅重寫了Post方法,意味着它支持異步分發,而不支持同步分發。我們采用如下的方式來使用FixedThreadSynchronizationContext。我們先創建一個FixedThreadSynchronizationContext對象,並采用線程池的方式同時執行5個異步操作。對於我們異步操作來說,我們先調用靜態方法SetSynchronizationContext將創建的這個FixedThreadSynchronizationContext對象設置為當前SynchronizationContext。然后調用Post方法將指定的操作分發給當前SynchronizationContext。置於具體的操作,它會打印出當前線程池線程和當前操作執行線程的ID。

class Program
{
    static async Task Main()
    {
         var synchronizationContext = new FixedThreadSynchronizationContext();
         for (int i = 0; i < 5; i++)
        {
            ThreadPool.QueueUserWorkItem(_ =>
            {
                SynchronizationContext.SetSynchronizationContext(synchronizationContext);
                Invoke();
            });
        }
        Console.Read();
        void Invoke()
        {
            var dispatchThreadId = Thread.CurrentThread.ManagedThreadId;
            SendOrPostCallback callback = _ => Console.WriteLine($"Pooled Thread: {dispatchThreadId}; Execution Thread: {Thread.CurrentThread.ManagedThreadId}");
            SynchronizationContext.Current.Post(callback, null);
        }
    }
}

這段演示程序執行之后會輸出如下所示的結果,可以看出從5個線程池線程分發的5個操作均是在FixedThreadSynchronizationContext綁定的那個線程中執行的。

image

三、ConfiguredTaskAwaitable方法

我知道很少人會顯式地使用SynchronizationContext上下文,但是正如我前面所說,在基於Task的異步編程中,SynchronizationContext上下文其實一直在發生作用。我們可以通過如下這個簡單的例子來證明SynchronizationContext的存在。如代碼片段所示,我們創建了一個FixedThreadSynchronizationContext對象並通過調用SetSynchronizationContext方法將其設置為當前SynchronizationContext。在調用Task.Delay方法(使用await關鍵字)等待100ms之后,我們打印出當前的線程ID。

class Program
{
    static async Task Main()
    {
        SynchronizationContext.SetSynchronizationContext(new FixedThreadSynchronizationContext());
        await Task.Delay(100);
        Console.WriteLine("Await Thread: {0}", Thread.CurrentThread.ManagedThreadId);
    }
}

如下所示的是程序運行之后的輸出結,可以看出在await Task之后的操作實際是在FixedThreadSynchronizationContext綁定的那個線程上執行的。在默認情況下,Task的調度室通過ThreadPoolTaskScheduler來完成的。顧名思義,ThreadPoolTaskScheduler會將Task體現的操作分發給線程池中可用線程來執行。但是當它在分發之前會先獲取當前SynchronizationContext,並將await之后的操作分發給這個同步上下文來執行。

image

如果不了解這個隱含的機制,我們編寫的異步程序可能會導致很大的性能問題。如果多一個線程均將這個FixedThreadSynchronizationContext作為當前SynchronizationContext,意味着await Task之后的操作都將分發給一個單一線程進行同步執行,但是這往往不是我們的真實意圖。其實這個問題很好解決,我們只需要調用等待Task的ConfiguredTaskAwaitable方法,並將參數設置為false顯式指示后續的操作無需再當前SynchronizationContext中執行。

class Program
{
    static async Task Main()
    {
        SynchronizationContext.SetSynchronizationContext(new FixedThreadSynchronizationContext());
        await Task.Delay(100).ConfigureAwait(false);
        Console.WriteLine("Await Thread: {0}", Thread.CurrentThread.ManagedThreadId);
    }
}

再次執行該程序可以從輸出結果看出await Task之后的操作將不會自動分發給當前的FixedThreadSynchronizationContext了。

image

四、再次回到開篇的例子

由於SynchronizationContext的存在,所以如果將開篇的例子修改成如下的形式是OK的,因為await之后的操作會通過SynchronizationContext分發到UI主線程執行。

partial class Form1
{  
    private void InitializeComponent()
    {
        ...
        this.Load += Form1_Load;
    }
    private async void Form1_Load(object sender, EventArgs e)
     {
         await Task.Delay(1000);
         Text = "Hello World";
     }
}

但是如果添加了ConfigureAwait(false)方法的調用,依然會拋出上面遇到的InvalidOperationException異常。

partial class Form1
{  
    private void InitializeComponent()
    {
        ...
        this.Load += Form1_Load;
    }

    private async void Form1_Load(object sender, EventArgs e)
     {
         await Task.Delay(1000).ConfigureAwait(false);
         Text = "Hello World";
     }
}

從執行上下文角度重新理解.NET(Core)的多線程編程[1]:基於調用鏈的”參數”傳遞
從執行上下文角度重新理解.NET(Core)的多線程編程[2]:同步上下文
從執行上下文角度重新理解.NET(Core)的多線程編程[3]:安全上下文


免責聲明!

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



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