又到了周末的code review環節,這次code review發現了一個對async/await的理解問題。讓我們直奔主題:
var foodsSearch = new FoodSearchService().SearchAsync(); var fruitsSearch = new FruitSearchService().SearchAsync(); var foods = await foodsSearch; foods.ForEach(f => Console.WriteLine("food:{0}", f.Name)); Console.WriteLine("done");
這是一段使用async/await的代碼,算得上是async/await的最佳使用實踐。問題出在大家對這段代碼理解各有不同,讓我們來看看如何理解這段代碼:
這個理解正確嗎?團隊的爭論焦點在步驟2上,大家錯誤的認為await關鍵字就是等待的意思,為了在第三步拿到結果,線程在步驟2處等待直到FoodSearchService返回結果。如果你也是這樣理解的,那么你應該看看下面的代碼如何理解?
var foodsSearch = new FoodSearchService().SearchAsync(); var fruitsSearch = new FruitSearchService().SearchAsync(); var foods = foodsSearch.Result; foods.ForEach(f => Console.WriteLine("food:{0}", f.Name)); Console.WriteLine("done");
剛才錯誤的理解正好是這段代碼的描述。那么第一段使用async/await的代碼如何理解?
使用await標記的方法調用(上面例子中的foodsSearch)不會阻塞主線程,主線程在步驟2不會等待。
為了說明這個結論,我們用下面的代碼模擬await(僅僅是模擬其行為,並不能真確執行),我沒有研究過await的實現,但是下面的代碼跟await具有相同的行為:
var foodsSearch = new FoodSearchService().SearchAsync(); var fruitsSearch = new FruitSearchService().SearchAsync(); var callback = foodsSearch as ICallBackRegister; callback.Register<List<Food>>(foods => { foods.ForEach(f => Console.WriteLine("food:{0}", f.Name)); Console.WriteLine("done"); }).Await(foodsSearch);
為了方便大家理解,我們寫一個簡單的ICallBackRegister實現:
public class CallbackRegister:ICallBackRegister { Action<object> _action; public ICallBackRegister Register<T>(Action<T> callback) { _action = o => callback((T) o); return this; } public void Await(Task task) { task.ContinueWith((result)=>_action(result)); } }
從上面模擬的代碼中我們可以得出兩個結論:
1、沒有任何阻塞主線程的代碼。
2、盡量推遲await的調用,能在步驟2使用await就不要在步驟1使用。因為一旦使用了await,后面所有的代碼都變成了await所調用對象的回調,無法跟之前的異步代碼並行。即便你在步驟1就使用了await,只能說FoodSearchService和FruitSearchService兩者不能並行,但是任然不會阻塞主線程——在主線程上永遠沒有等待這一說。
我們再看張圖來解釋一下這期間發生的事情:
設想這樣的代碼放在一個GUI中Button的click事件中,由於await不會阻塞主線程,界面再不會有假死的情況發生。
this.BackColor = Color.Aquamarine; this.btnAsyncAwait.BackColor = Color.Blue; var operationA = new LongTimeOperationA().GetValueAsync(); var operationB = new LongTimeOperationB().GetValueAsync(); var valueA =await operationA; Text = valueA;
同樣的道理,在web mvc編程中,如果controller和EF中全程使用async/await,此時假設用戶有一個請求過來,IIS會從線程池中取出一個線程來響應用戶請求,由於主線程沒有任何阻塞,所以IIS會很快將線程回收到了線程池中。當EF返回數據並且返回ActionResult時,IIS再次從線程池中拿出一個線程來對用戶請求做響應。所以
正是由於async/await不會阻塞主線程,我們才說async/await會提高IIS的響應能力。
另外async/await的使用並不會提升訪問數據庫的效率,該花多長時間還得花多長時間。
最后我們給出Task.Result版本的click事件,由於調用Task.Result會阻塞主線程,所以你可以看到界面假死的現象。
this.BackColor = Color.Beige; this.btnTaskResult.BackColor = Color.BlueViolet; var operationA=new LongTimeOperationA().GetValueAsync(); var operationB=new LongTimeOperationB().GetValueAsync(); var valueA = operationA.Result; Text = valueA;
代碼下載:download