本文繼續上篇未完成的討論,通過將Lambda還原成最普通的代碼段,來解釋上篇提出的疑問。並更正上篇中一些不太正確的寫法。最后會給出無需等待Async方法返回值時,對Async方法使用await的建議,供大家參考。
第一篇傳送門:await使用中的阻塞和並發
首先我們比較以下三段代碼,其中第一和第三可以做到並發執行,第二個是線性的執行。
//並發 public async Task Await3Task() { var task3 = Delay3000Async(); var task2 = Delay2000Async(); var task1 = Delay1000Async(); await task3; await task2; await task1; } //非並發 public async Task Await3DelayAsync() { await Delay3000Async(); await Delay2000Async(); await Delay1000Async(); } //並發,這里甚至可以把var task3等去掉,直接調用xxxAsync(),只是會出現警告的波浪罷了 public void NoAwait3Task() { var task3 = Delay3000Async(); var task2 = Delay2000Async(); var task1 = Delay1000Async(); }
//這里補充一下調用的三個Async方法
public async Task Delay3000Async() { await Task.Delay(3000); Console.WriteLine(3000); Console.WriteLine(DateTime.Now); } public async Task Delay2000Async() { await Task.Delay(2000); Console.WriteLine(2000); Console.WriteLine(DateTime.Now); } public async Task Delay1000Async() { await Task.Delay(1000); Console.WriteLine(1000); Console.WriteLine(DateTime.Now); }
這里我們可以看出,await和並發木有關系,隱式的並發執行是由async方法決定的。而await是用於主動的阻塞,期望等待方法結束才繼續運行時使用。
以下2個方法的執行結果是一樣的,都可以並發執行。可以看出,把僅僅希望並發執行,不需要返回結果的方法丟到List里,然后Foreach是毫無意義的……所以上篇其實是干了一些蠢事情……
public void AwaitTaskList() { var tasks = new List<Task> { Delay3000Async(), Delay2000Async(), Delay1000Async() }; tasks.ForEach(async _ => await _); } public void NoAwaitTaskList() { var tasks = new List<Task> { Delay3000Async(), Delay2000Async(), Delay1000Async() }; }
上篇提到Task在創建的時候,就已經開始運行了。而await僅僅是出現在需要等待結果的地方。所以如果無需等待,就不要寫await了……貌似上篇又干了一些蠢事……
//非並發 public async Task Await3Func() { Func<Task> func3 = Delay3000Async; Func<Task> func2 = Delay2000Async; Func<Task> func1 = Delay1000Async; await func3(); await func2(); await func1(); } //並發 public void NoAwait3Func() { Func<Task> func3 = Delay3000Async; Func<Task> func2 = Delay2000Async; Func<Task> func1 = Delay1000Async; func3(); func2(); func1(); }
同時我本人對List<Func<Task>>尚未創建Task卻可以並發表示疑問,接下來給出解答。下面分別貼出了使用Lambda和不使用的情況。我們可以清楚的看到Lambda表達式具體幫我們省略了什么。
//使用Lambda public void AwaitFuncTaskList() { var funcList = new List<Func<Task>>() { Delay3000Async, Delay2000Async, Delay1000Async }; funcList.ForEach(async _ => await _()); } //不使用Lambda public void AwaitFuncTaskListNoLambda() { var funcList = new List<Func<Task>>() { Delay3000Async, Delay2000Async, Delay1000Async }; //funcList.ForEach(AwaitAction()); foreach(var func in funcList) { AwaitAction()(func); } } public Action<Func<Task>> AwaitAction() { return async _ => await _(); }
根據上文的總結,可以看出雖然await造成了阻塞,但並不是在主線程等待,所以我們幸運的並發了……
再看下面一段,我干脆拿掉了await,毫無疑問的並發執行了。上篇讓人汗顏的事情貌似還干了不少,好在我臉皮厚,不會刪掉前一篇的隨筆,哈哈哈哈……
public void NoAwaitFuncTaskList() { var funcList = new List<Func<Task>>() { Delay3000Async, Delay2000Async, Delay1000Async }; funcList.ForEach(_ => _()); } public void NoAwaitFuncTaskListNoLambda() { var funcList = new List<Func<Task>>() { Delay3000Async, Delay2000Async, Delay1000Async }; //funcList.ForEach(NoAwaitAction()); foreach(var func in funcList) { NoAwaitAction()(func); } } public Action<Func<Task>> NoAwaitAction() { return _ => _(); }
仔細看一下可以發現,為了懶惰而使用的ForEach其實增加了多余的一層Action<Func<Task>>,如果直接使用foreach會是如下的情況:
public void NoAwaitFuncTaskWithoutForeachExtension() { var funcList = new List<Func<Task>>() { Delay3000Async, Delay2000Async, Delay1000Async }; foreach (var func in funcList) { func(); } }
接下來是總結陳述:
- async用於異步,可以優美的替代Thread、BackgroundWorker和Task.Run等寫法。
- await用於等待。一定是在你主動希望阻塞並等待返回結果時才使用。
- 在async方法里,Task在創建時就開始運行了。
- 寫Lamdba別把自己寫暈了……