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

在開始吹牛以前,老周說兩個故事。異步

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

因此嘛,對於這些人,我把林妹妹的一首詩送給他們:工具

無故弄筆是何人?開發工具

做踐.net過輕狂。spa

不悔自家無見識,.net

卻將醜語怪他人。線程

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

============================================blog

 

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

一、變量必須是多個線程共享的,若是是線程範圍內的局部變量就不須要了。

二、但願每一個線程都能讀寫獨立的變量值。

今天,老周再介紹一個功能和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的功能了吧。

 

本文示例下載地址

相關文章
相關標籤/搜索