寫得不好,關于Async和await我在GitHub博客里重新總結了下,可以直接看這個 https://631320085.github.io/2017/02/07/Async_await.html
前兩天剛感受了下泛型接口的in和out,昨天就開始感受神奇的異步方法Async/await,當然順路也看了眼多線程那幾個。其實多線程異步相關的類單個用法和理解都不算困難,但是異步方法Async/await這東西和Task攪到了一起就有點花花腸子。要單說用法其實也好理解,也有不少文章寫了。看過上一篇的同學知道,不弄清楚來龍去脈,這世界總感覺不夠高清。異步方法究竟怎么個異步法,為什這樣設計,有什么意義?昨天想到今天,感覺終于算是講得通了,一點愚見記下來分享給大家。
先不著急直奔主題,看看多線程那一家子,再看他們和Async怎么搞基的。
1 線程和線程池Thread&ThreadPool
最基本的線程調用工具
//線程 //線程初始化時執行方法可以帶一個object參數,為了傳入自定義參數,所以執行需單獨調用用于傳參。 Console.WriteLine("執行線程"); Thread th = new Thread((objParam) => { Console.WriteLine("線程啟動,執行匿名方法,有無參數{0}", objParam != null); }); th.IsBackground = true; object objP = new object(); th.Start(objP); //線程池 //線程池初始化執行方法必須帶一個object參數,接受到的值是系統默認NULL(不明),所以初始化完成自動調用 Console.WriteLine("執行線程池"); ThreadPool.QueueUserWorkItem((objparam) => { Console.WriteLine("線程池加入的匿名方法被執行。"); });
執行結果:
可以看到這Thread和ThreadPool執行不影響主進程執行。Thread和ThreadPool接受的都是委托類型,所以可以單獨定義方法在初始化的時候傳入,接受的委托都返回void,所以都不能在線程里有返回值。Thead是單開線程,ThreadPool是使用系統的線程池所以性能更好。參數相關注釋里有寫,其他特性我們不深究,我們知道這兩的原理就是使用線程執行無返回值的方法的即可。
2 并行循環Parallel
是用多個線程執行循環的工具
int result = 0; int lockResult = 0; object lb = new object(); //并行循環 //并行應該用于一次執行多個相同任務,或計算結果和循環的游標沒有關系只和執行次數有關系的計算 Console.WriteLine("執行并行循環"); Parallel.For(0, 10, (i) => { result = result + 2; //lock只能lock引用類型,利用引用對象的地址唯一作為鎖,實現lock中的代碼一次只能一個線程訪問 //lock讓lock里的代碼在并行時變為串行,盡量不要在parallel中用lock(lock內的操作耗時小,lock外操作耗時大時,并行還是起作用) lock(lb) { lockResult = lockResult + 2; Thread.Sleep(100); Console.WriteLine("i={0},lockResult={1}", i, lockResult); } Console.WriteLine("i={0},result={1}", i, result); });
說明一下,為了驗證并行循環的執行過程,加入lock玩了一下。lock為什么只能lock引用對象是我推測的,如有偏差,概不負責。
執行結果:
Parallel用法很簡單,就是Parallel.For(游標開始值, 游標結束, int參數的Action),傳入action的方法接受的int參數就是當前執行的游標。
跑題開始-------------------------------(手賤要在并行里寫lock還要sleep剛好形成規律,以下是寫博時發現的,沒興趣的同學可以跳過)
通過結果我們可以看出,首先執行順序是隨機的,可以猜到一次是把游標的取值分別當參數傳給多個線程執行action即可。后面的結果也驗證了這一點,lockResult不用說,不管多少線程到這都得排隊執行,所以結果遞增。再看result,上來就變成了10,可以推出遇到lock之前已經被加了5次,那么應該是一次4個線程嘍(大家肯定覺得應該是5個,開始我也是這樣覺得,往下看)。
再看result其實也不是沒規律,可以看出從10到20也是遞增,但到了20就不增加了(因缺思亭)。我們模擬下(按5個線程模擬不符合結果,我就直接按合理的情況推一遍)。
1 首先可以4個線程ABCD同時執行,都到了lock這停住,那這時result被加了4次是8。
2 然后一個線程A執行lock里的代碼,其他的BCD等待(不是sleep仍然占用cpu),執行完輸出lockResult=2(第一行)。
這時繼續往下應該輸出result=8對吧,為什么是result=10。注意lock里有一個Thread.Sleep(100),這就是關鍵。在lock里sleep會怎樣,當前線程A釋放cpu 100ms,這時就可以再來一個線程E執行到lock這也停住了,result是不是就是10了。
3 這個線程A醒來優先級最高擠掉一個線程往下繼續輸出result=10(第二行)。這時剛才被擠掉的線程又恢復占用cpu狀態,就是BCDE四個線程。
4 同理,BCDE四個等待線程的又有一個進入lock然后又sleep,又可以有一個線程來把result加2,這時循環這個過程,result也呈現出規律。
5 為什么result后面幾次都是20,因為總共執行10次,首先四個線程執行了4次,然后一個新線程執行第5次后,第1次執行的線程才輸出第5次執行后的結果,第2次輸出第6次。。。第6次輸出第10次(第6行就是result=20),后面四次已經執行過result加2,所以只輸出結果20。
如果把Thread.Sleep(100)去掉result就不再有這么明顯的規律。因為sleep讓cpu可以釋放與lock等待共同作用讓線程執行形成一個先后順序的隊列。sleep放到lock外也不行,sleep會釋放cpu,放到lock外,沒有lock占用cpu,lock前就不一定執行了幾次。
為什么一次是四個線程呢,很容易想到,我CPU四核的。。。就這么簡單。。
跑題結束---------------------------------
通過以上分析,并行是個什么東西大家應該有所了解了,繼續。
3 任務Task
一個可以有返回值(需要等待)的多線程工具
//任務 Task.Run(() => { Thread.Sleep(200); Console.WriteLine("Task啟動執行匿名方法"); }); Console.WriteLine("Task默認不阻塞"); //獲取Task.Result會造成阻塞等待task執行 int r = Task.Run(() => { Console.WriteLine("Task啟動執行匿名方法并返回值"); Thread.Sleep(1000); return 5; }).Result; Console.WriteLine("返回值是{0}", r);
執行結果:
用法如上,好像使用的是線程池。傳入方法不能有參數,可以有返回值。要獲得結果,要在Run()(返回Task<T>類型)之后調用Task<T>類型的Result屬性獲取。可以看出,獲取結果時,Task是會阻塞當前進程的,等待線程執行完畢才繼續。
Task好用,關鍵點就是有返回值,可以獲取結果。
------------------------------------------------關于多線程就扯這么多,終于進入主題Async異步方法--------------------------------------------------------------
異步方法Async&await&Task
一些點:
1 異步方法需要Async關鍵字修飾
2 異步方法的返回類型只能是void或Task<T>
3 返回值類型是T時,異步方法返回類型必須是Task<T>
4 await可以用于async方法和 async方法中的task(通過3、4兩點大家應該能猜到,異步方法本身其實就是一個Task或者說和自己內部的Task在同一線程)
5 只有異步方法內使用了 (await關鍵詞描述的)(有返回值的線程Task)才能提現異步方法的優勢
寫了一個異步方法,一個普通方法進行對比測試。異步方法正確使用的代碼如下:(后面幾次測試在此基礎上稍作修改即可)
//異步方法 public async Task<int> MethodA(DateTime bgtime, int i) { int r = await Task.Run(() => { Console.WriteLine("異步方法{0}Task被執行", i); Thread.Sleep(100); return i * 2; }); Console.WriteLine("異步方法{0}執行完畢,結果{1}", i, r); if (i == 49) { Console.WriteLine("用時{0}", (DateTime.Now - bgtime).TotalMilliseconds); } return r; } //普通方法 public int MethodC(DateTime bgtime, int i) { int r = Task.Run(() => { Console.WriteLine("普通多線程方法{0}Task被執行", i); Thread.Sleep(100); return i * 2; }).Result; Console.WriteLine("普通方法{0}執行完畢,結果{1}", i, r); if (i == 49) { Console.WriteLine("用時{0}", (DateTime.Now - bgtime).TotalMilliseconds); } return r; }
調用代碼:
public static void ACTest() { Asy_ClassA asy = new Asy_ClassA(); DateTime pbgtime = DateTime.Now; for (int i = 0; i < 50; i++) { asy.MethodC(pbgtime, i); Console.WriteLine("普通方法{0}調用完成", i); } DateTime abgtime = DateTime.Now; for (int i = 0; i < 50; i++) { asy.MethodA(abgtime, i); Console.WriteLine("異步方法{0}調用完成", i); } }
測試開始!------------------------------------------------------------------------------------------------
第一次:都獲取Task的返回結果,異步方法使用await獲取,普通方法使用Task.Run().Result獲取。測試結果:
可以發現普通方法由于阻塞執行都是按順序執行,多線程失去意義。異步方法則并行執行,重要的是計算結果一樣。所以在方法內需要使用Task結果時,異步方法使用await不阻塞調用進程優勢明顯。
第二次:異步方法中不使用await,使用和普通方法一樣的Task.Run().Result獲取結果。測試結果:
可以看到用時和執行順序都一樣。所以沒有await的情況下,異步方法等待Task結果時一樣會阻塞調用進程。
第三次:都只調用Task執行,不獲取結果。測試結果:
可以看到,不管是普通方法還是異步方法都是多個線程并行執行,所以不獲取結果的時,異步方法和普通多線程方法性能一樣。
在這次測試基礎上,讓異步方法await一個不返回結果的Task會發現,異步方法內還是會等待Task執行完畢。所以只要使用await不管是方法還是Task,有無返回結果,后面的代碼都要等待其執行完畢。
第四次:把ACTesct方法改成Async異步方法,再用await調用asy.MethodA()異步方法。測試結果:
await只能在異步方法中使用(為什么這樣設計后面分析),所以ACTest需要改成Async。可以看到,異步方法調用時被await了一樣會等待。所以異步方法應該沒有返回值或者調用時不關注返回結果才有效。
測試完畢!-----------------------------------------------------------------------------------
重要的總結:
【意義】異步方法的意義就是保證一個進程使用多線程多次執行一個方法時,不會因為其中某一次執行阻塞調用進程
【原理】利用方法內Task調用新線程,await使方法內等待Task結果時調用進程不被阻塞,多次調用相當于多個線程并行。(不被阻塞的原因應該是異步方法本身就和內部的Task跑在一個線程里)
【區別】普通方法只用Task也可以并行,當方法內需要Task返回值時,等待Task結果就會阻塞調用進程。
【應用】主要應用在沒有返回值,使用線程且需要線程返回結果的方法。
一些分析:
1.異步方法有返回值會怎樣?
因為異步方法返回類型是Task<T>,所以獲取返回值只能await或者.Result,兩者都會讓當前方法等待。
2.那么異步方法是不是沒有作用了?
如果是用.Result獲取,那么是。如果是await就不一定了。await只能在async方法中使用,所以await獲取異步方法返回值的方法也是異步的,再往上最終只能肯定是一個普通方法調用異步方法。是否有用取決于普通方法內調用最上層異步方法的方式。
3.為什么返回值類型是T,方法返回類型需要是Task<T>?
要達到異步方法內等待線程結果不阻塞調用進程,這個方法本身就應該在線程中執行。所以不管返回類型是什么,放到Task中運行后返回的是Task<T>。這樣被調用時相當于一個Task.Run(),也就可以實現異步方法await了。
4.為什么要實現異步方法await可等待?
異步方法的await其實第二點已經分析了,實現異步方法await可以允許異步方法內繼續調用異步方法,把異步操作從底層向上層傳遞。而能夠傳遞到的最上層是什么,是static void Main(),所以最終還是普通方法調用異步方法。也就是說不能繼續使用await等待異步方法的結果了,當最上層不關注返回結果時,不管內部有多少次await異步方法的調用,依然還是多線程的并行。如果最上層非要關注異步方法的返回結果,用.Result獲取其結果,那我無話可說。
5.關于Async和await。
await其實不光是一個簡單的讓下一行代碼等待異步方法或Task結果的關鍵字。應該理解成一個擴大當前Task代碼執行范圍的命令。
從最開始的await Task讓整個異步方法B都能在Task中運行(所以普通方法調用異步方法B時,B內await Task結果就不會阻塞調用進程)。
到異步方法A中await異步方法B讓異步方法A和B都在同一Task內運行(所以普通方法調用異步方法A時,A內await異步方法B的結果和B內await Task的結果就不會阻塞調用進程)
Async用于標識一個方法是異步方法,約束其返回類型為Task<T>。也就說內部可以使用await,且方法本身是放到Task中執行的,所以代碼返回類型T,方法的返回類型卻是Task<T>。
最后一定要區別異步方法和普通多線程方法的用處,他們的關鍵區別就是是否需要單獨等待線程的執行結果。不要把異步方法當多線程方法用了。
----------------------------------------------------------------完--------------------------------------------------------------
終于算是完了,研究了兩天,寫了兩天。第一天寫到很晚,草草結尾,很多同學可能沒理解。今天又再次編輯,重新總結分類,排版。應該能講清楚了吧,這次真的真的寫完了。
覺得有幫助的同學可以推薦或者頂一下。
文章列表