線程池之ThreadPool類與輔助線程 - <第二篇>


一、CLR線程池

  管理線程開銷最好的方式:

  1. 盡量少的創建線程並且能將線程反復利用(線程池初始化時沒有線程,有程序請求線程則創建線程);
  2. 最好不要銷毀而是掛起線程達到避免性能損失(線程池創建的線程完成任務后以掛起狀態回到線程池中,等待下次請求);
  3. 通過一個技術達到讓應用程序一個個執行工作,類似於一個隊列(多個應用程序請求線程池,線程池會將各個應用程序排隊處理);
  4. 如果某一線程長時間掛起而不工作的話,需要徹底銷毀並且釋放資源(線程池自動監控長時間不工作的線程,自動銷毀);
  5. 如果線程不夠用的話能夠創建線程,並且用戶可以自己定制最大線程創建的數量(當隊列過長,線程池里的線程不夠用時,線程池不會坐視不理);

  微軟早就替我們想到了,為我們實現了線程池。

  CLR線程池並不會在CLR初始化時立即建立線程,而是在應用程序要創建線程來運行任務時,線程池才初始化一個線程。

  線程池初始化時是沒有線程的,線程池里的。線程的初始化與其他線程一樣,但是在完成任務以后,該線程不會自行銷毀,而是以掛起的狀態返回到線程池。直到應用程序再次向線程池發出請求時,線程池里掛起的線程就會再度激活執行任務。

  這樣既節省了建立線程所造成的性能損耗,也可以讓多個任務反復重用同一線程,從而在應用程序生存期內節約大量開銷。

通過CLR線程池所建立的線程總是默認為后台線程,優先級數為ThreadPriority.Normal

二、工作者線程與I/O線程

  CLR線程池分為工作者線程(workerThreads)與I/O線程(completionPortThreads)兩種:

  • 工作者線程是主要用作管理CLR內部對象的運作,通常用於計算密集的任務。
  • I/O(Input/Output)線程主要用於與外部系統交互信息,如輸入輸出,CPU僅需在任務開始的時候,將任務的參數傳遞給設備,然后啟動硬件設備即可。等任務完成的時候,CPU收到一個通知,一般來說是一個硬件的中斷信號,此時CPU繼續后繼的處理工作。在處理過程中,CPU是不必完全參與處理過程的,如果正在運行的線程不交出CPU的控制權,那么線程也只能處於等待狀態,即使操作系統將當前的CPU調度給其他線程,此時線程所占用的空間還是被占用,而並沒有CPU處理這個線程,可能出現線程資源浪費的問題。如果這是一個網絡服務程序,每一個網絡連接都使用一個線程管理,可能出現大量線程都在等待網絡通信,隨着網絡連接的不斷增加,處於等待狀態的線程將會很消耗盡所有的內存資源。可以考慮使用線程池解決這個問題。

  線程池的最大值一般默認為1000、2000。當大於此數目的請求時,將保持排隊狀態,直到線程池里有線程可用。

  使用CLR線程池的工作者線程一般有兩種方式:

  • 通過ThreadPool.QueueUserWorkItem()方法;
  • 通過委托;

  要注意,不論是通過ThreadPool.QueueUserWorkItem()還是委托,調用的都是線程池里的線程。

三、ThreadPool類常用方法

  通過以下兩個方法可以讀取和設置CLR線程池中工作者線程與I/O線程的最大線程數。

  1. ThreadPool.GetMax(out in workerThreads,out int completionPortThreads);
  2. ThreadPool.SetMax(int workerThreads,int completionPortThreads);

  若想測試線程池中有多少線程正在投入使用,可以通過ThreadPool.GetAvailableThreads(out in workThreads,out int conoletionPortThreads)方法。

方法 說明
GetAvailableThreads 剩余空閑線程數
GetMaxThreads 最多可用線程數,所有大於此數目的請求將保持排隊狀態,直到線程池線程變為可用
GetMinThreads 檢索線程池在新請求預測中維護的空閑線程數。
QueueUserWorkItem 啟動線程池里得一個線程(隊列的方式,如線程池暫時沒空閑線程,則進入隊列排隊)
SetMaxThreads 設置線程池中的最大線程數
SetMinThreads 設置線程池最少需要保留的線程數
    class Program
    {
        static void Main(string[] args)
        {
            int i = 0;
            int j = 0;
            //前面是輔助(也就是所謂的工作者)線程,后面是I/O線程
            ThreadPool.GetMaxThreads(out i, out j);
            Console.WriteLine(i.ToString() + "   " + j.ToString()); //默認都是1000

            //獲取空閑線程,由於現在沒有使用異步線程,所以為空
            ThreadPool.GetAvailableThreads(out i, out j);
            Console.WriteLine(i.ToString() + "   " + j.ToString()); //默認都是1000

            Console.ReadKey();
        }
    }

四、各種調用線程池線程的方法

  1、通過QueueUserWorkItem啟動工作者線程

  ThreadPool線程池中有兩個重載的靜態方法可以直接啟動工作者線程:

  •   ThreadPool.QueueUserWorkItem(waitCallback);
  •   ThreadPool.QueueUserWorkItem(waitCallback,Object);

  先把WaitCallback委托指向一個帶有Object參數的無返回值方法,再使用ThreadPool.QueueUserWorkItem(WaitCallback)就可以一步啟動此方法,此時異步方法的參數被視為null。

  下面來試下用QueueUserWorkItem啟動線程池里的一個線程。注意哦,由於是一直存在於線程池,所以不用new Thread()。

    class Program
    {
        static void Main(string[] args)
        {
            //工作者線程最大數目,I/O線程的最大數目
            ThreadPool.SetMaxThreads(1000, 1000);   
            //啟動工作者線程
            ThreadPool.QueueUserWorkItem(new WaitCallback(RunWorkerThread));

            Console.ReadKey();
        }

        static void RunWorkerThread(object state)
        {
            Console.WriteLine("RunWorkerThread開始工作");
            Console.WriteLine("工作者線程啟動成功!");
        }
    }

  輸出:

  

  使用第二個重載方法ThreadPool.QueueUserWorkItem(WaitCallback,object)方法可以把object對象作為參數傳送到回調函數中。

    class Program
    {
        static void Main(string[] args)
        {
            Person p = new Person(1,"劉備");
            //啟動工作者線程
            ThreadPool.QueueUserWorkItem(new WaitCallback(RunWorkerThread), p);
            Console.ReadKey();
        }

        static void RunWorkerThread(object obj)
        {
            Thread.Sleep(200);
            Console.WriteLine("線程池線程開始!");
            Person p = obj as Person;
            Console.WriteLine(p.Name);
        }
    }

    public class Person
    {
        public Person(int id,string name) { Id = id; Name = name; }
        public int Id { get; set; }
        public string Name { get; set; }
    }

  輸出結果如下:

  

  通過ThreadPool.QueueUserWork啟動工作者線程非常方便,但是WaitCallback委托指向的必須是一個帶有object參數的無返回值方法。所以這個方法啟動的工作者線程僅僅適合於帶單個參數和無返回值的情況。

  那么如果要傳遞多個參數和要有返回值又應該怎么辦呢?那就只有通過委托了。

  2、BeginInvoke與EndInvoke委托異步調用線程

  異步調用委托的步驟如下:

  1. 建立一個委托對象,通過IAsyncResult BeginInvoke(string name,AsyncCallback callback,object state)異步調用委托方法,BeginInvoke方法除最后的兩個參數外,其他參數都是與方法參數相對應的。
  2. 利用EndInvoke(IAsyncResult--上一步BeginInvoke返回的對象)方法就可以結束異步操作,獲取委托的運行結果。
    class Program
    {
        //除了最后兩個參數,前面的都是你可定義的
        delegate string MyDelegate(string name,int age);
        static void Main(string[] args)
        {
            //建立委托
            MyDelegate myDelegate = new MyDelegate(GetString);
            //異步調用委托,除最后兩個參數外,前面的參數都可以傳進去
            IAsyncResult result = myDelegate.BeginInvoke("劉備",22, null, null);  //IAsynResult還能輪詢判斷,功能不弱

            Console.WriteLine("主線程繼續工作!");

            //調用EndInvoke(IAsyncResult)獲取運行結果,一旦調用了EndInvoke,即使結果還沒來得及返回,主線程也阻塞等待了
            //注意獲取返回值的方式
            string data = myDelegate.EndInvoke(result);
            Console.WriteLine(data);

            Console.ReadKey();
        }

        static string GetString(string name, int age)
        {
            Console.WriteLine("我是不是線程池線程" + Thread.CurrentThread.IsThreadPoolThread);
            Thread.Sleep(2000);
            return string.Format("我是{0},今年{1}歲!",name,age);
        }
    }

  輸出如下:

  

  這種方法有一個缺點,就是不知道異步操作什么時候執行完,什么時候開始調用EndInvoke,因為一旦EndInvoke主線程就會處於阻塞等待狀態。

  3、IAsyncResult輪詢

  為了克服上面提到的缺點,此時可以好好利用IAsyncResult提高主線程的工作性能,IAsyncResult有如下成員。

public interface IAsyncResult
{
  object AsyncState {get;}       //獲取用戶定義的對象,它限定或包含關於異步操作的信息。
  WailHandle AsyncWaitHandle {get;}  //獲取用於等待異步操作完成的 WaitHandle。
  bool CompletedSynchronously {get;} //獲取異步操作是否同步完成的指示。
  bool IsCompleted {get;}        //獲取異步操作是否已完成的指示。
}

  示例如下:

    class Program
    {
        delegate string MyDelegate(string name,int age);
        static void Main(string[] args)
        {
            MyDelegate myDelegate = new MyDelegate(GetString);
            IAsyncResult result = myDelegate.BeginInvoke("劉備",22, null, null);

            Console.WriteLine("主線程繼續工作!");

            //比上個例子,只是利用多了一個IsCompleted屬性,來判斷異步線程是否完成
            while (!result.IsCompleted)
            {
                Thread.Sleep(500);          
                Console.WriteLine("異步線程還沒完成,主線程干其他事!");
            }

            string data = myDelegate.EndInvoke(result);
            Console.WriteLine(data);

            Console.ReadKey();
        }

        static string GetString(string name, int age)
        {
            Thread.Sleep(2000);
            return string.Format("我是{0},今年{1}歲!",name,age);
        }
    }

  輸出如下:

  

  以上例子,除了IsCompleted屬性外,還可以使用AsyncWaitHandle如下3個方法實現同樣輪詢判斷效果:

  • WaitOne:判斷單個異步線程是否完成;
  • WaitAny:判斷是否異步線程是否有指定數量個已完成;
  • WaitAll:判斷是否所有的異步線程已完成;

  WaitOne:

  //比上個例子,判斷條件由IsCompleted屬性換成了AsyncWaitHandle,僅此而已
  while (!result.AsyncWaitHandle.WaitOne(200))
  {
      Console.WriteLine("異步線程沒完,主線程繼續干活!");
  }

  WaitAny:

  //是否完成了指定數量
  WaitHandle[] waitHandleList = new WaitHandle[] { result.AsyncWaitHandle };
  while (WaitHandle.WaitAny(waitHandleList, 200) > 0)
  {
      Console.WriteLine("異步線程完成數未大於0,主線程繼續甘其他事!");
  }

  WaitAll:

  WaitHandle[] waitHandleList = new WaitHandle[] { result.AsyncWaitHandle };
  //是否全部異步線程完成
  while (!WaitHandle.WaitAll(waitHandleList, 200))
  {
      Console.WriteLine("異步線程未全部完成,主線程繼續干其他事!");
  }

  4、IAsyncResult回調函數

  使用輪詢方式來檢測異步方法的狀態非常麻煩,而且影響了主線程,效率不高。能不能異步線程完成了就直接調用實現定義好的處理函數呢?

  有,還是強大的IAsyncResult對象。

    class Program
    {
        delegate string MyDelegate(string name, int age);

        static void Main(string[] args)
        {
            //建立委托
            MyDelegate myDelegate = new MyDelegate(GetString);
            //倒數第二個參數,委托中綁定了完成后的回調方法
            IAsyncResult result1 = myDelegate.BeginInvoke("劉備",23, new AsyncCallback(Completed), null);
            //主線程可以繼續工作而不需要等待
            Console.WriteLine("我是主線程,我干我的活,不再理你!");
            Thread.Sleep(5000);
            //Console.ReadKey();
        }

        static string GetString(string name, int age)
        {
            Thread.CurrentThread.Name = "異步線程";
            //注意,如果不設置為前台線程,則主線程完成后就直接卸載程序了
            //Thread.CurrentThread.IsBackground = false;
            Thread.Sleep(2000);
            return string.Format("我是{0},今年{1}歲!", name, age);
        }

        //供異步線程完成回調的方法
        static void Completed(IAsyncResult result)
        {
            //獲取委托對象,調用EndInvoke方法獲取運行結果
            AsyncResult _result = (AsyncResult)result;
            MyDelegate myDelegaate = (MyDelegate)_result.AsyncDelegate;
            //獲得參數
            string data = myDelegaate.EndInvoke(_result);
            Console.WriteLine(data);
            //異步線程執行完畢
            Console.WriteLine("異步線程完成咯!");
            Console.WriteLine("回調函數也是由" + Thread.CurrentThread.Name + "調用的!");
        }
    }

  輸出如下:

  

  注意:

  1. 回調函數依然是在輔助線程中執行的,這樣就不會影響主線程的運行。
  2. 線程池的線程默認是后台線程。但是如果主線程比輔助線程優先完成,那么程序已經卸載,回調函數未必會執行。如果不希望丟失回調函數中的操作,要么把異步線程設為前台線程,要么確保主線程將比輔助線程遲完成。

  到目前為止,BeginInvoke("劉備",23, new AsyncCallback(Completed), null)還有最后一個參數沒用過的。那么最后一個參數是用來干什么?傳參:

namespace 控制台___學習測試
{
    class Program
    {
        delegate string MyDelegate(string name, int age);

        static void Main(string[] args)
        {
            Person p = new Person(2,"關羽");

            //建立委托
            MyDelegate myDelegate = new MyDelegate(GetString);
            //最后一個參數的作用,原來是用來傳參的
            IAsyncResult result1 = myDelegate.BeginInvoke("劉備", 23, new AsyncCallback(Completed), p);
            //主線程可以繼續工作而不需要等待
            Console.WriteLine("我是主線程,我干我的活,不再理你!");
            Console.ReadKey();
        }

        static string GetString(string name, int age)
        {
            Thread.CurrentThread.Name = "異步線程";
            //注意,如果不設置為前台線程,則主線程完成后就直接卸載程序了
            Thread.CurrentThread.IsBackground = false;
            Thread.Sleep(2000);
            return string.Format("我是{0},今年{1}歲!", name, age);
        }

        //供異步線程完成回調的方法
        static void Completed(IAsyncResult result)
        {
            //獲取委托對象,調用EndInvoke方法獲取運行結果
            AsyncResult _result = (AsyncResult)result;
            MyDelegate myDelegaate = (MyDelegate)_result.AsyncDelegate;
            //獲得參數
            string data = myDelegaate.EndInvoke(_result);
            Console.WriteLine(data);

            Person p = result.AsyncState as Person;
            Console.WriteLine("傳過來的參數是:" + p.Name);
            //異步線程執行完畢
            Console.WriteLine("異步線程完成咯!");
            Console.WriteLine("回調函數也是由" + Thread.CurrentThread.Name + "調用的!");
        }
    }

    public class Person
    {
        public Person(int id, string name)
        {
            Id = id;
            Name = name;
        }

        public int Id
        {
            get;
            set;
        }

        public string Name
        {
            get;
            set;
        }
    }
}

  輸出如下:

  


免責聲明!

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



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