程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 異步方法的意義何在,Async和await以及Task的愛恨情仇,還有多線程那一家子。,asyncawait

異步方法的意義何在,Async和await以及Task的愛恨情仇,還有多線程那一家子。,asyncawait

編輯:C#入門知識

異步方法的意義何在,Async和await以及Task的愛恨情仇,還有多線程那一家子。,asyncawait


  前兩天剛感受了下泛型接口的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 只有異步方法內使用了 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了,也就實現了異步方法並行,結果就是:異步方法內可以等待或者不等待另一個異步方法的調用,而最上層的異步方法是由普通方法調用不能等待所以並行,且最上層的異步方法的返回值沒有意義。

  最後一定要區別異步方法和普通多線程方法,他們的關鍵區別就是是否可以單獨等待線程的執行結果。不要把異步方法當多線程方法用了。

  覺得有幫助的同學可以推薦或者頂一下,這兩天研究這個都要精盡人亡了,我睡了。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved