【.NET深呼吸】基於異步上下文的本地變量(AsyncLocal)


在開始吹牛之前,老周說兩個故事。

第一個故事是關於最近某些別有用心的人攻擊.net的事,其實我們不用管它們,只要咱們知道自己是.net愛好者就行了,咱們就是因為熱愛.net才會選擇它。這些人在這段時間攻擊.net,估計和.net的開源、跨平台有關,並且,據說VS 2015 Update 1會進一步深化和擴展全平台,估計有些人是沉不住氣了,畢竟他們用的開發工具是比VS落后了四個多世紀的。最近又出了個Visual Studio Dev Essentials計划。

所以嘛,對於這些人,我把林妹妹的一首詩送給他們:

無端弄筆是何人?

作踐.net太輕狂。

不悔自家無見識,

卻將丑語怪他人。

接下來說說第二個故事。或許不少應屆畢業生都在准備或者已經在找實習單位,或找工作了,於是有朋友私信老周,希望老周說說簡歷如何做的事情。這個嘛,一來,老周不是簡歷專家;二來,在本文中不好展開去談,過一兩天吧,老周找時間再寫篇爛文,專門說說這個事;三來,僅為一家之言,以供參考。

============================================

 

好了,與主題無關的話說完了,下面開始說正事。記得在X月前,老周寫過有關ThreadLocal的文章,也就是基於線程的本地變量存儲。使用這個ThreadLocal的前提是:

1、變量必須是多個線程共享的,如果是線程范圍內的局部變量就不需要了。

2、希望每個線程都能讀寫獨立的變量值。

今天,老周再介紹一個功能和ThreadLocal類似的東東——AsyncLocal。

這個主要是用於保存異步等待上下文中的共享變量的值。從C# 5開始,引入了相當簡便的異步等待語法,即await關鍵字調用異步方法,允許異步等待。

即代碼在使用await關鍵字調用異步方法后,當前程序會等待異步方法返回后才會繼續執行,但在這個等待過程中,不會阻塞當前線程,這比起編寫委托來回調方便多了。

異步方法是基於Task的自動線程調度,在異步上下文的切換過程中,有可能會導致數據丟失。比如,在await調用前,對某個變量賦了值,而這個變量是多個線程共享的;當await調用返回后,有可能當前代碼仍然處於先前的線程上,但也有可能被調度到其他線程上。這種情況一般發生在與應用程序UI線程無關的代碼上,如果異步操作是由UI啟動的,通常情況下不會調動異步上下文的線程,然而,如果異步操作是非UI觸發的,典型的如在Main入口處啟動的,這就很有可能出現異步上下文處於不同的線程上的情形。

 

這樣描述太抽象,很難懂,沒事,給大家看一個例子就知道了。

先定義一個異步方法:

        static async Task RunAsync()
        {
            // 輸出當前線程的ID
            Console.WriteLine($"異步等待前,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
            // 開始執行異步方法,並等待完成
            await Task.Delay(50);
            // 異步等待完成后,再次輸出當前線程的ID
            Console.WriteLine($"異步等待后,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
        }


異步方法中,調用了Task.Delay方法,這個方法也是可以異步等待的,因此用await關鍵字來等待50毫秒。

然后,在Main入口處調用以上異步方法。

        static void Main(string[] args)
        {
            // 聲明一個委托實例
            Action act = async () =>
             {
                 await RunAsync();
             };

            // 執行委托
            act();

            Console.Read();
        }

我這里是先聲明一個Action委托實例,並通過Lambda表達式調用異步方法,並且異步等待其完成。因為使用了await關鍵字的方法上必須標注async修飾符,以說明該方法中出現異步等待代碼,但是,Main入口方法上是不允許添加async修飾符的,所以,我就用一個委托來調用。

運行這個例子,你會有驚奇發現,請看,有圖有真相。

 

看到沒,await等待前,當前的線程是8,異步等待回來后,當前線程就被自動調度到10上了。

            == 在線程8上
            await Task.Delay(50);
            == 在線程10上

 從代碼上看,await前后是連續的,但實際上,在執行階段,它們已經處於不同的線程上了。

那么,我就想啊,如果在此種情況下使用ThreadLocal變量會發生什么事情。試試看。

        // 線程共享變量
        static ThreadLocal<int> local = new ThreadLocal<int>();

        static void Main(string[] args)
        {
            // 聲明一個委托實例
            Action act = async () =>
             {
                 await RunAsync();
             };

            // 執行委托
            act();

            Console.Read();
        }

        static async Task RunAsync()
        {
            // 給共享變量賦值
            local.Value = 53000;
            // 輸出變量的值
            Console.WriteLine($"異步等待前:{nameof(local)} = {local.Value}");
            await Task.Delay(50); //異步等待
            // 異步等待回來,再次輸出變量的值
            Console.WriteLine($"異步等待后:{nameof(local)} = {local.Value}");
        }

 

上面例子使用了ThreadLocal聲明線程間共享變量,在異步方法中,先給這個變量賦值為53000,然后await開始等待,等待返回后,再次輸出變量的值。

好,注意看,意外發生了。

 

喲,有朋友估計會尖叫了,這是咋回事?await前不是給共享的變量賦了值嗎,為什么等待返回后值會變回默認值0呢。前面老周說了,等待前,等待后是有可能處於不同的線程上,而ThreadLocal是為每個線程保存獨立的值的。

假設,設置local值為53000是在線程A上執行的,那么,local變量為線程A保留了值53000;當代碼執行到await關鍵字一行后,開始異步等待,而等待返回后,當前代碼可能被調度到線程B上了。而53000是為線程A所存儲的值,對於線程B,未賦值,所以就得到默認的值0。

 

很顯然,ThreadLocal是不適合在異步上下文中使用的。下面就請出今天的主角——AsyncLocal。

把上面的代碼中的ThreadLocal改為AsyncLocal。

        // 線程共享變量
        static AsyncLocal<int> local = new AsyncLocal<int>();
         ……

 

然后,再運程序,看圖。

 

看到了吧,這下子好了,53000在異步上下文中被保留了。

現在,你明白了AsyncLocal的功能了吧。

 

本文示例下載地址

 


免責聲明!

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



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