【C#多線程】2.線程池簡述+兩種傳統的異步模式


線程池簡述+兩種傳統的異步編程模式

1.線程池簡述

  首先我們要明確一點,編程中講的線程與平時我們形容CPU幾核幾線程中的線程是不一樣的,CPU線程是指邏輯處理器,比如4核8線程,講的是這個cpu有8個邏輯處理器,可以同時處理8個線程。我們編程中講的線程在計算機中可以有許多許多,如下圖所示,這些線程並不是都在執行狀態,他們平時大部分都是休眠狀態,只有進程去調用他們時,他們才是激活狀態。線程通過他們的ThreadState(線程狀態)屬性告訴CPU,它們是否需要被CPU去執行。比如有2000個線程,其中有20個線程的線程狀態屬性為“待執行”,那么CPU的邏輯處理器就會在空閑時根據線程的優先級去執行線程(4核8線程的CPU最多同時執行8個線程),正在執行的線程狀態屬性會被改為“正在執行”,當該線程執行結束后,其線程狀態屬性會被改為“休眠”,此時CPU就不會再理他們。

  • 什么是C#線程池呢?

  顧名思義,線程池就是放線程的池子,我們在運行任意.NET程序時,都會在CLR(你可以把他理解為軟件后台)生成一個線程池,池內已經new出來了很多的Thread實例,我們在需要型線程的時候不用自己new,直接從池子里拿現成的Thread實例即可,用完后這個Thread實例會被自動還回線程池!線程池中的線程對象數量與我們計算機有關,具體數字我忘了,反正是CPU核心越多,邏輯處理器越多,那么線程池的線程就越多,我們一般不用管池內有多少個線程(一般是足夠你用的),即使線程池的線程都在被占用狀態,此時你再從線程池拿線程時,線程池也會自動new新增一個線程給你。

  • 為什么要使用C#線程池呢?

  因為new一個Thread是比較耗費資源並且執行較慢的行為,比如我們在一個1000次的循環中,每個循環都要new出一個Thread進行某些任務的處理,會使得任務執行緩慢,並且計算機內存蹭蹭上漲。我們不如直接在每次循環中從線程池獲取一個線程,用完再放回去,這樣的處理不僅速度快,對內存也沒有任何影響。

2.線程池的使用(簡單講解)

  因為線程池在.NET4.0后新出的Task類及Async與await關鍵字出現后就不怎么用了,這里僅僅簡單講一講線程池的用法。

  直接看代碼:

        //創建一個線程執行的方法
        public static void DoSth(object obj) { //輸出當前執行線程的ID
            Console.WriteLine((string)obj+Thread.CurrentThread.ManagedThreadId); Thread.Sleep(500);//線程睡眠5秒
 } static void Main(string[] args) { for (int i = 0; i < 1000; i++) { //-----------------非簡寫方式----------------- //WaitCallback是一個委托(有一個Object類型參數,無返回值)
                WaitCallback callBack = new WaitCallback(DoSth); //QueueUserWorkItem只支持WaitCallback作為參數,第二個參數是傳入委托方法的參數
                ThreadPool.QueueUserWorkItem(callBack, "abc"); //-----------------lambda簡寫方式-----------------
                ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => { //輸出當前執行線程的ID
                    Console.WriteLine((string)obj + Thread.CurrentThread.ManagedThreadId); Thread.Sleep(500);//線程睡眠5秒
                }),"abc"); } }

  這里補充講一下ThreadPool.QueueUserWorkItem方法,這個方法從作用上講是從線程池獲取一個新線程去執行一個委托方法。但是為什么它的方法名是QueueUserWorkItem而非GetValueableThread這樣的名稱呢?因為QueueUserWorkItem的實質其實是將委托方法傳入線程池的一個任務隊列中,線程池中的空閑線程負責去對任務隊列中的線程進行執行,這才是它實質的運行邏輯。

  注意:ThreadPool.QueueUserWorkItem只能接受“有且只有一個Object類型,且無返回值的委托方法”。

3.異步編程簡介

  同步編程:我們平時不用多線程的時候基本就是同步編程模式,代碼從上到下依次執行。

  關於什么是異步編程模式,首先我們看一段代碼:

        //聲明一個執行一次耗費5分鍾的方法
        public static void Spend5Min() { for(int i=0;i<300;i++) { Thread.Sleep(1000); } }
     //主方法
static void Main(string[] args) { Thread t1 = new Thread(Spend5Min); t1.Start();//將Spend5Min()這個方法交給t1線程執行 Spend5Min();//主線程去執行Spend5Min()方法 }

  這段代碼會怎么運行呢?

  首先主線程會將Spend5Min這個方法交給t1線程去運行,然后自己也開始運行Spend5Min這個方法。這時候,主線程與t1線程基本會同時執行Spend5Min方法。

  上面這種模式就是異步編程模式,異步編程的實質就是代碼並非是從上到下依次執行的,而是在代碼中間產生一個新線程分支,去執行新任務,主線程只負責將任務交給他,然后就不管它,繼續往下執行。(而不是等t1線程把Spend5Min方法執行完畢后,主線程再開始執行Spen5Min)。

  這里有一個的誤區,許多人覺得只要用了多線程就是異步編程,請看下面的代碼:

        static void Main(string[] args) { Thread t1 = new Thread(Spend5Min); t1.Start();//將Spend5Min()這個方法交給t1線程執行
            t1.Join();//讓主線程等待t1線程執行完畢再繼續執行。
            
       Spend5Min();//主線程去執行Spend5Min()方法 }

  因為t1.Join方法,讓主線程再此處會等待t1線程執行完畢再繼續執行,這種編程模式其實依舊是同步編程模式,因為它依舊是從上到下依次執行的,上面這段代碼可以說等同於下面這段代碼。

        static void Main(string[] args) { Spend5Min(); Spend5Min(); }

4.傳統的異步編程模式APM

  C# .NET最早出現的異步編程模式被稱為APM(Asynchronous Programming Model)。這種模式主要由一對Begin/End開頭的方法組成。BeginXXX方法用於異步啟動一個耗時任務,EndXXXEndXXX用來處理BeginXXX所返回的值(IAsyncResult對象)。BeginXXX方法和EndXXX方法之間的信息通過一個IAsyncResult對象來傳遞,IAsyncResult 對象是異步的核心,簡單的說,他是存儲異步返回值+異步狀態信息的一個接口,也可以用它來結束當前異步。

  .NET中一個典型的例子是System.Net命名空間中的HttpWebRequest類里的BeginGetResponse和EndGetResponse這對方法:

IAsyncResult BeginGetResponse(AsyncCallback callback, object state)

  上面的BeginGetResponse用來開啟一個異步方法,下面這個方法用於處理上面異步方法返回的值,只有執行完了EndXXX,一個完整的異步操作才算完成(EndXXX一般寫在Beginxxxx的回調函數中)。

WebResponse EndGetResponse(IAsyncResult asyncResult);

  注意: BeginInvoke和EndInvoke必須成對調用.即使不需要返回值,但EndInvoke還是必須調用,否則可能會造成內存泄漏。

  APM使用簡單明了,雖然代碼量稍多,但也在合理范圍之內。APM兩個最大的缺點是不支持進度報告以及不能方便的“取消”。
  示例:   

  (1)同步調用異步方法

  下面代碼介紹了APM異步方法的錯誤用法,雖然使用了異步方法,但是其效果依舊是同步模式,所以稱下面的代碼是同步方式調用異步方法。

    public class Program { public delegate int AddHandler(int a, int b); public static int Add(int a, int b) { Thread.Sleep(3000); return a+b; Console.WriteLine("異步方法執行完畢"); } static void Main() { AddHandler handler = new AddHandler(Add); //BeginInvoke: 委托(delegate)的一個異步方法的開始 //第三個函數為回調函數,BeginInvoke完成后自動執行
            IAsyncResult result = handler.BeginInvoke(1,2,null,null); Console.WriteLine("在前面沒執行完前我這就執行完了");//在異步方法還沒執行完之前,此句代碼就會被執行 //返回異步操作結果() //因為result還沒有被異步方法返回,主線程代碼會卡在這個地方,直到異步方法把result返回(這就導致與同步代碼一樣了)
 Console.WriteLine(handler.EndInvoke(result)); Console.ReadLine(); } }

  代碼解釋:handler.BeginInvoke僅僅只負責開始異步執行委托方法,並返回當前異步result對象。只有主動執行handler.EndInvoke(異步result)才可獲取到方法return中的結果。
  代碼效果:可以看到,主線程並沒有等待,而是直接向下運行了。但是問題依然存在,當主線程運行到EndInvoke時,如果這時BeginInvoke沒有執行結束(result還沒被算出來),這時為了等待調用結果,主線程依舊會被阻塞。

  (2)正確使用APM異步模式

  思路:將handler.EndInvoke放在handler.BeginInvoke的回調函數中執行,這樣當BeginInvoke執行完畢后,后台線程繼續執行回調函數(包括handler.EndInvoke方法)直接輸出結果,不會阻塞主線程。

    public class Program { public delegate int AddHandler(int a, int b); public static int Add(int a, int b) { Thread.Sleep(3000); return a+b; Console.WriteLine("異步方法執行完畢"); } static void Main() { AddHandler handler = new AddHandler(Add); //第三個函數為回調函數,BeginInvoke完成后自動執行 //第四個函數定義異步執行result完成后的狀態
            IAsyncResult result = handler.BeginInvoke(1,2,new AsyncCallback(MyCallback),"AsycState:OK"); Console.WriteLine("在前面沒執行完前我這就執行完了"); Console.ReadLine(); } //異步回調:異步中執行的回調函數
        static void MyCallback(IAsyncResult result) { //result 是“加法Add()方法”的返回值 //AsyncResult 是IAsyncResult接口的一個實現類,要引用命名空間:System.Runtime.Remoting.Messaging //AsyncDelegate 屬性可以強制轉換為用戶定義的委托的實際類。
            AddHandler handler = (AddHandler)((AsyncResult)result).AsyncDelegate; Console.WriteLine(handler.EndInvoke(result)); Console.WriteLine(result.AsyncState); } }

  補充:因為委托的BeginInvoke中第4個參數可以放入任意對象,一般用於包含關於異步操作的信息,所以為了簡化回調函數,我們可以直接將委托對象傳遞到回調函數:

IAsyncResult result = handler.BeginInvoke(1,2,new AsyncCallback(AddComplete),AddHandler);

  這時result.AsyncState就裝着AddHandler委托對象了,回調函數可簡化為:

        static void AddComplete(IAsyncResult result) { AddHandler handler = (AddHandler)result.AsyncState; Console.WriteLine(handler.EndInvoke(result)); 。。。。。 }

  補充:如何在普通方法中創建回調函數?代碼如下:

        public void Method(參數1,參數2,Action<string> CallBackHandler) { //正常執行
            string result = ...;//得到結果 //將結果傳入回調函數中
 CallBackHandler.Invoke(result); }

5.傳統的異步編程模式EAP

  在C# .NET第二個版本中,增加了一種新的異步編程模型EAP(Event-based Asynchronous Pattern),EAP模式的異步代碼中,典型特征是一個以"Async"結尾的"方法"和以"Completed"結尾的"事件"。XXXCompleted事件將在異步處理完成時被觸發,在事件的處理函數中可以操作異步方法的結果。往往在EAP代碼中還會存在名為CancelAsync的方法用來取消異步操作,以及一個ProgressChenged結尾的事件用來匯報操作進度。通過這種方式支持取消和進度匯報也是EAP比APM更有優勢的地方。EAP中取消機制沒有可延續性,並且不是很通用。

  .NET2.0中新增的BackgroundWorker可以看作EAP模式的一個例子。另一個使用EAP的例子是被HttpClient所取代的WebClient類(新代碼應該使用HttpClient而不是WebClient)。WebClient類中通過DownloadStringAsync方法開啟一個異步任務,並有DownloadStringCompleted事件供設置回調函數,還能通過CancelAsync方法取消異步任務。

  因為APM與EAP異步編程模式目前在新代碼中基本不用了,所以這里就隨便講講,后續博客中將詳細的講解對基於Task及Async與await關鍵字的TAP異步模式。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM