在開始吹牛之前,老周說兩個故事。
第一個故事是關於最近某些別有用心的人攻擊.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的功能了吧。