前兩天剛感受了下泛型接口的in和out,昨天就開始感受神奇的異步方法Async/await,當然順路也看了眼多線程那幾個。其實多線程異步相關的類單個用法和理解都不算困難,但是異步方法Async/await這東西和Task攪到了一起就有點花花腸子。要單說用法其實也好理解,也有不少文章寫了。看過上一篇的同學知道,不弄清楚來龍去脈,這世界總感覺不夠高清。異步方法究竟怎麼個異步法,為什這樣設計,有什麼意義?昨天想到今天,感覺終於算是講得通了,一點愚見記下來分享給大家。
先不著急直奔主題,看看多線程那一家子,再看他們和Async怎麼搞基的。
最基本的線程調用工具
//線程 //線程初始化時執行方法可以帶一個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是使用系統的線程池所以性能更好。參數相關注釋裡有寫,其他特性我們不深究,我們知道這兩的原理就是使用線程執行無返回值的方法的即可。
是用多個線程執行循環的工具
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四核的。。。就這麼簡單。。
跑題結束---------------------------------
通過以上分析,並行是個什麼東西大家應該有所了解了,繼續。
一個可以有返回值(需要等待)的多線程工具
//任務 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異步方法--------------------------------------------------------------
一些點:
1 異步方法需要Async關鍵字修飾
2 異步方法的返回類型只能是void或Task<T>
3 只有異步方法內使用了 await關鍵詞描述的 有返回值的 線程Task才有效
4 await只能用在Async方法裡的Task上,await會讓當前方法等待Task執行完畢再執行
5 返回值類型是T時,方法返回類型就是Task<T>
既然異步方法一定要用Task才有效,就寫了一個用了Task的普通方法和一個異步方法測試,如下:
//【意義】異步方法的意義就是保證一個進程使用多線程多次執行一個方法時,不會因為其中某一次執行阻塞調用進程 //【原理】利用方法內Task調用新線程,await使方法內等待Task結果時調用進程不被阻塞,多次調用相當於多個線程並行。 //【區別】普通方法只用Task,當方法內需要Task返回值時,等待Task結果就會阻塞調用進程。 //【應用】主要應用在沒有返回值,操作耗時長(用Task)且需要Task返回結果的方法。(不需要Task返回值的話和普通方法調用Task性能一樣,因為普通方法也沒有阻塞進程的風險了) // (有返回值調用時await獲取結果就重蹈阻塞進程的覆轍了) public async Task<int> MethodA(DateTime bgtime, int i) { int r = await Task.Run(() => { Console.WriteLine("異步方法{0}Task被執行", i); return i * 2; }); Thread.Sleep(100); 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); return i * 2; }).Result; Thread.Sleep(100); Console.WriteLine("普通方法{0}執行完畢,結果{1}", i, r); if (i == 49) { Console.WriteLine("用時{0}", (DateTime.Now - bgtime).TotalMilliseconds); } return r; }
測試結果:
可以發現普通方法由於阻塞執行都是按順序執行,多線程失去意義。異步方法則並行執行,重要的是計算結果一樣。
重要的結論都寫在注釋裡了。相信透過代碼和結果大家或多或少能理解了吧,以及上面提到的幾個點為什麼這樣設計透過結論也能推出一二。我就不一一就結論進行分析和驗證了,相信我為了得到這些結論做的驗證不會少。
還有一點,為什麼返回值類型是T,方法返回類型需要是Task<T>,我的推測是這樣的:要達到異步方法內等待線程結果不阻塞調用進程,這個方法本身就應該在線程中執行。所以不管返回值類型是什麼,都被替換成類型Task<T>。這樣被調用時相當於一個Task.Run(),也就可以實現異步方法await了(雖然這樣就失去異步的意義但有原因)。比如自己在一個普通方法裡寫異步方法調用AsyncMethod(),系統會給你提示說不加await程序會繼續執行,建議加await等待其結果,你要是加了後就報錯了,說await只能用在async方法裡,是不是有點蹊跷。只能用在async方法裡的話,那就是說只有異步方法內才能await AsyncMethod()(等待異步方法調用的結果)。所以最終調用異步方法的肯定是一個普通方法,就不能await了,也就實現了異步方法並行,結果就是:異步方法內可以等待或者不等待另一個異步方法的調用,而最上層的異步方法是由普通方法調用不能等待所以並行,且最上層的異步方法的返回值沒有意義。
最後一定要區別異步方法和普通多線程方法,他們的關鍵區別就是是否可以單獨等待線程的執行結果。不要把異步方法當多線程方法用了。
覺得有幫助的同學可以推薦或者頂一下,這兩天研究這個都要精盡人亡了,我睡了。