【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關鍵字出現後就不怎麼用了,這裏僅僅簡單講一講線程池的用法。async

  直接看代碼:異步編程

        //建立一個線程執行的方法
        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類型,且無返回值的委託方法」。spa

3.異步編程簡介

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

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

        //聲明一個執行一次耗費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異步模式。

相關文章
相關標籤/搜索