【C#多執行緒】2.執行緒池簡述+兩種傳統的非同步模式

  • 2019 年 11 月 6 日
  • 筆記

執行緒池簡述+兩種傳統的非同步編程模式

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非同步模式。