【C#非同步】非同步多執行緒的本質,上下文流轉和同步

2023-03-02 18:01:35

引言

    net同僚對於async和await的話題真的是經久不衰,這段時間又看到了關於這方面的討論,最終也沒有得出什麼結論,其實要弄懂這個東西,並沒有那麼複雜,簡單的從本質上來講,就是一句話,async 和await非同步的本質就是狀態機+執行緒環境上下文的流轉,由狀態機向前推進執行,上下文進行環境切換,在狀態機向前推進的時候第一次的movenext會將當前執行緒的環境上下文儲存起來,然後由TaskScheduler排程是否去執行緒池拿新執行緒執行這個task,等到後續推進到最後的movenext的時候,裡面設定好結果,異常之後,回撥則需要執行在呼叫await之前的環境上下文中去,這裡說的是環境上下文,而並非是執行緒,所以當前環境上下文在await之前是A執行緒的上下文,在遇到await結束之後可能是B執行緒的環境上下文,並且非同步是非同步,執行緒是執行緒,非同步不一定多執行緒,這兩個不是等價的,針對async和await的原始碼刨析可以看一下之前寫的部落格https://www.cnblogs.com/1996-Chinese-Chen/p/15594498.html,這篇文章針對原始碼講了一部分,可能不是很明瞭,只講了async await執行的一個順序對於環境上下文沒有過多的描述,接下來,我會講一些環境上下文,同步上下文的知識,以及在cs程式中,框架對於同步上下文的封裝。

環境上下文ExecutionContext

    ExecutionContext表示管理當前執行緒的執行上下文。針對此類,官網的解釋是該 ExecutionContext 類為與邏輯執行執行緒相關的所有資訊提供單個容器。 在.NET Framework中,這包括安全上下文、呼叫上下文和同步上下文。 在 .NET Core 中,不支援安全上下文和呼叫上下文,但是,模擬上下文和區域性通常通過執行上下文流動。 簡單來說,這個類就是存放當前執行緒所有環境資訊的容器,在net framework 和net core中,略有不同,後者不包括同步上下文,關於同步上下文和ExecutionContext,可以看看官網的另一篇比較好的文章https://devblogs.microsoft.com/pfxteam/executioncontext-vs-synchronizationcontext/ 這篇文章,對於async await非同步和上下文做了更加詳細的解釋。

    那麼在剛開始我們說了非同步的本質之一就是上下文流轉,那麼什麼是流轉呢,怎麼流轉,這個類代表的存放當前執行緒資訊的容器,那我們複製一份這個容器,然後放到另一個執行緒去,那另一個執行緒就可以獲取到我們上一個執行緒內部的所有的資訊,簡單理解就是,搬家的時候我把我的所有東西打包放在我的新房子,那這個新房子也有了我搬家之前的那些資訊,這個就是上下文流轉,接下來,我們看一下實際在程式碼中的例子

 public class TestTask
    {
        public static AsyncLocal<int> id;
    }
  private async void button1_Click(object sender, EventArgs e)
        {
            //var sss = new MyTask(() => { Console.WriteLine(111); });
            //await sss;
            var exce = ExecutionContext.IsFlowSuppressed();
            TestTask.id = new AsyncLocal<int>() { Value = 1 };
            // var asynclo=ExecutionContext.SuppressFlow();
            var con1 = ExecutionContext.Capture();
            var a = ExecutionContext.SuppressFlow();
            exce = ExecutionContext.IsFlowSuppressed();
            await Task.Delay(1000);
            var con2 = ExecutionContext.Capture();
            ExecutionContext.Run(con2, s =>
            {
                var sss = TestTask.id.Value;
            }, null);
            await Task.Delay(1000);
            ExecutionContext.Restore(con1);
            var sssa = TestTask.id.Value;
        }

    在上面的程式碼中,我首先定義了一個AsyncLocal 存放int型別的一個變數,在winform中我介面新增一個按鈕,在點選事件中寫下了如下程式碼,在第一行程式碼中呼叫了ExecutionContext.IsFlowSuppressed方法,這個方法是判斷是否停止當前上下文的流轉,

在剛開始執行的時候,這個返回結果是False,說明我們沒有停止流轉,是可以正常流轉,在第二行程式碼中,我們給AsyncLocal變數賦值,設定Value為1;第三行中,我們使用了ExecutionContext.Capture方法,這個方法是捕獲當前上下文資訊,然後賦值給了con1變數,在往下走,我們呼叫了SuppressFlow方法,這個方法是我們阻止了當前上下文的流轉,也就是說這個上下文是和await之後的上下文是不一樣的,然後我們在判斷IsFlowSuppressed的時候返回的就是true了,停止了流轉,然後我們非同步Delay1秒,然後我們捕獲非同步之後的當前執行緒的上下文資訊,然後在這裡我們捕獲我們這個執行緒的上下文資訊,接下來呼叫了ExecutionContext.Run方法,這個方法是將第二個引數的委託程式碼,執行在指定的上下文中去,這塊這個run方法我們用不用其實都不影響演示效果,在這程式碼中,我們獲取到id.Value就和上面的不一樣獲取的是預設值0,而不是上面定義的1,這就是因為我們停止了上下文流轉,導致await前後不是同一個上下文,所以獲取不到這個Value,如果我們不呼叫SuppressFlow,那在await之後就是上一個的上下文資訊,獲取到的Value也就是原來的1,在往下走,我們在Delay一下,在呼叫Restore方法,這個方法是將當前執行緒的上下文替換為指定的上下文資訊,將指定上下文資訊還原到當前執行緒,然後在獲取的Value就是1了。

    在ExectuionContext方法中有幾個方法,就是Capture這個是靜態捕獲當前上下文資訊,CreateCopy這個是實體方法,返回當前上下文資訊的副本,IsFlowSuppressFlow判斷是否停止上下文流轉,SuppressFlow是停止上下文流轉,Restore是將捕獲的上下文資訊還原到當前執行緒,當然了還有一個方法,和SuppressFlow方法對應,一個停止一個是恢復,叫RestoreFlow回覆當前上下文在非同步執行緒之間的流動,但是呢在async這個場景中是不適合這種情況的,是有一個報錯,這個報錯是當前上下文並沒有停止上下文流轉,這個是為什麼呢,且聽我娓娓道來。

    我們都知道,執行緒的發展是Thread,Threadpool,再到現在的Task,然後Task是基於Threadpool封裝的,那麼我們在使用await Task之後的執行緒,是由Threadpool指定的,那他指定的執行緒不一定是await前的執行緒,就導致了你await之後恢復上下文流動的時候提示你上下文並沒有停止流動,因為執行緒不一樣導致的這個問題,就是說你SuppressFlow是另一個執行緒,await之後的是另一個執行緒,你RestoreFlow另一個執行緒,那肯定會報錯啊,所以我們是需要使用Restore方法,將我們之前捕獲的上下文資訊還原到當前執行緒,這樣,我們後續在獲取Value的時候就可以獲取到結果了。

    這塊還需要講解一個問題就是,在上一段中,我們說了,Task的執行緒都是由Threadpool分配的,就會導致某些程式碼執行的執行緒是由Threadpool分配,那這個問題就導致了原有的Thread方面的東西是不能做執行緒資料傳遞的,例如,ThreadLocal,ThreadStaticAttribute特性,這些都是不能玩了的,因為使用Task,Threadpool的執行緒,我們不知道前後是否一樣,那ThreadLocal和ThreadStatic 每一個執行緒是每一個執行緒的資料我們就會獲取不到,這一點,大家在使用的時候還需要了解到。

SynchronizationContext 

    上面講的ExecutionContext可以叫是執行緒環境上下文,SynchronizationContext提供在各種同步模型中傳播同步上下文的基本功能。可以稱它為執行緒同步上下文。如果ExectuionContext是整個環境資訊的容器,那這個類是暴露給你整個環境資訊的介面,雖然Execution也可以做不同執行緒之間的同步,但是你把所有的都暴露那總歸是不好的,你能把你家的東西都讓他知道嗎,很顯然不能,這個SynchronizationContext每個執行緒都可以設定自己的同步上下文資訊,可以重寫這個類,也可以就使用這個類去進行非同步或者同步的分派資訊到某個執行緒的上下文中去,同步使用Send方法,傳入SendOrPostCallBack委託和委託需要的引數。

    如果我們線上程中獲取SynchronizationContext.Current的時候為空,null,我們可以建立一個SynchronizationContext的變數,var context=new SynchronizationContext();然後呼叫SynchronizationContext.SetSynchronizationContext(context);為當前執行緒設定同步上下文,需要在其他執行緒同步的時候 只需要context.Post方法或者context.Send方法即可同步。

    此外,在CS程式中,winform,wpf都由針對SynchronizationContext類重寫以便實現框架層面的需要,因為在cs程式中,所有控制元件的建立修改刪除,等操作,都應該是由UI執行緒去完成,如果跨執行緒則會報錯,同時在cs程式中使用了async和await,在await之後的環境上下文和同步上下文都是await之前的資料,所以在cs中await之後操作UI是不會有任何問題的,如果是需要在子執行緒中操作UI控制元件,則需要獲取SynchronizationContext.Current物件獲取當前同步上下文,或者使用winform重寫之後的類WinformSynchronizationContext.Current獲取同步上下文物件,然後去進行Post或者Send操作UI控制元件就不會報錯。

    今天在微信群討論的時候,群友們在討論跨執行緒操作的問題,便說到了這塊,另外有個老哥說到,在子執行緒建立控制元件物件新增到表單中,然後在操作的時候會報錯,針對這個,我測試了之後,在子執行緒中建立TextBox,主執行緒給Text賦值,不會報錯導致一場,然後我就猜測控制元件都是繼承於Control類,那應該是Control類和SynchronizationContext類做了關聯,導致雖然是子執行緒建立的物件,但是同樣是屬於主執行緒的,隨後我去翻看了原始碼,驗證了我的猜想。在下面的圖中,如果我們在子執行緒new TextBox(),是走到了Contrl()這個構造方法,然後走到了internal Control的構造方法,引數autoInstallSyncContext是true,

 

    然後呼叫了WindowsFormSynchronizationContext.InstallIfNeeded()方法,在這個方法我們最終看到子執行緒建立的控制元件最終還是屬於UI執行緒的同步上下文的,為此我用程式碼做了驗證。

 

 

 

 

    在程式碼中執行這段程式碼,在Task.Run裡面加入斷點,就可以看到,在new TextBox之前,SynchronizationContext.Current獲取到的是null,在之後獲取到的是WindowsFormsSynchronizationContext的物件,由此可以看出所有的Control控制元件,哪怕都在子執行緒中建立,其也依舊屬於UI執行緒。

await AddText();this.Controls.Add(TextBox) ;
JextBox.Text = "111」;

 

 public  Task AddText()
        {
            var con=WindowsFormsSynchronizationContext.Current;
            return  Task.Run(() =>
            {
                var c = SynchronizationContext.Current;
                TextBox = new TextBox();

                var b = SynchronizationContext.Current;
            });
        }

 

 結語

    今天的分享就到此結束了,對於async和await,更深層次的其實還是上下文流轉,用不用新執行緒,是有TaskScheduler決定,執行緒複用是有ThreadPool決定,並且,非同步不一定開啟新執行緒,那不然委託非同步,控制元件非同步 是不是都開了新執行緒,賣個關子,有待你們去進行驗證,如有疑問,歡迎大家進群討論6406277,或者822074314都行,群內有很多大佬可以一起學習進步,另外也可以看群裡有沒有叫四川觀察的,基本上就是咯,咱們下次再見