天天看點

淺談.NET下的多線程和并行計算(五)線程池基礎上

池(Pool)是一個很常見的提高性能的方式。比如線程池連接配接池等,之是以有這些池是因為線程和資料庫連接配接的建立和關閉是一種比較昂貴的行為。對于這種昂貴的資源我們往往會考慮在一個池容器中放置一些資源,在用的時候去拿,在不夠的時候添點,在用完就歸還,這樣就可以避免不斷的建立資源和銷毀資源。

如果您做過相關實驗的話可能會覺得不以為然,似乎開1000個線程也用不了幾百毫秒。我們要這麼想,對于一個高并發的環境來說,每一秒假設有100個請求,每個請求需要使用(開和關)10個線程,也就是一秒需要處理1000個線程的開和關,每個線程獨立堆棧1M,可以想象在這一秒中記憶體配置設定和回收是多麼誇張,這個開銷不能說不昂貴。

首先,要了解線程池線程分為兩類工作線程和IO線程,可以單獨設定最小線程數和最大線程數:

ThreadPool.SetMinThreads(2, 2);
ThreadPool.SetMaxThreads(4, 4);      

最大線程數很好了解,就是線程池最多建立這些線程,如果最大4個線程,現在這4個線程都在運作的話,後續進來的線程隻能排隊等待了。那麼為什麼有最小線程一說法呢?其實之是以使用線程池是不希望線程在建立後運作結束後了解回收,這樣的話以後要用的時候還需要建立,我們可以讓線程池至少保留幾個線程,即使沒有線程在工作也保留。上述語句我們設定線程池一開始就保持2個工作線程和2個IO線程,最大不超過4個線程。

至于線程池的使用相當簡單先來看一段代碼:

for (int i = 0; i < totalThreads; i++)
{
    ThreadPool.QueueUserWorkItem(o =>
    {
        Thread.Sleep(1000);
        int a, b;
        ThreadPool.GetAvailableThreads(out a, out b);
        Console.WriteLine(string.Format("({0}/{1}) #{2} : {3}", a, b, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("mm:ss")));
    });
}
Console.WriteLine("Main thread finished");
Console.ReadLine();      

代碼裡面用到了一個事先定義的靜态字段:

static readonly int totalThreads = 10;      

代碼運作結果如下:

淺談.NET下的多線程和并行計算(五)線程池基礎上

每一個線程都休眠一秒然後輸出目前線程池可用的工作線程和IO線程以及目前線程的托管ID和時間。我們通過這段代碼可以發現線程池的幾個特性:

1) 線程池中的線程都是背景線程,如果沒有在主線程使用ReadLine的話,程式馬上會退出。

2) 線程池一開始就占用了2個線程,一秒後占用了4個線程,工作線程将會由3-6四個線程來處理。

3) 線程池最多使用了4個工作線程和0個IO線程。

那麼,我們如何知道線程池中的線程都運作結束了呢,可以想到上文用過的Monitor結構:

Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < totalThreads; i++)
{
    ThreadPool.QueueUserWorkItem(o =>
    {
        Thread.Sleep(1000);
        int a, b;
        ThreadPool.GetAvailableThreads(out a, out b);
        Console.WriteLine(string.Format("({0}/{1}) #{2} : {3}", a, b, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("mm:ss")));
        lock (locker)
        {
            runningThreads--;
            Monitor.Pulse(locker);
        }

    });
}

lock (locker)
{
    while (runningThreads > 0)
        Monitor.Wait(locker);
}

Console.WriteLine(sw.ElapsedMilliseconds);
Console.ReadLine();      

程式中用到了兩個輔助字段:

static object locker = new object();
      
static int runningThreads = totalThreads;      

程式運作結果如下:

淺談.NET下的多線程和并行計算(五)線程池基礎上

我們看到,10個線程使用了3.5秒全部執行完畢。20個線程呢?

淺談.NET下的多線程和并行計算(五)線程池基礎上

需要6秒。細細分析這2個圖我們不難發現,新的線程不是在不夠用的時候立即建立而是延遲了0.5秒左右的時間,這是因為線程池會等待一下看是不是有線程在這段時間内可用,如果實在沒有的話再建立。其實可以這麼了解這6秒,前一秒隻有2個線程,後4秒有4個線程執行了16個,最後1秒又隻有2個線程了,是以一共是2+4*4+2=20,6秒處理了20個線程。

ThreadPool還有一個很有用的方法可以注冊一個信号量,我們發出信号後所有關聯的線程才執行,否則就一直等待,還可以指定等待的時間:

首先定義信号量和存儲結果的字段:

static ManualResetEvent mre = new ManualResetEvent(false);
static int result = 0;      
程式如下:      
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < totalThreads; i++)
{
    ThreadPool.RegisterWaitForSingleObject(mre, (state, istimeout) =>
        {
            Thread.Sleep(1000);
            int a, b;
            ThreadPool.GetAvailableThreads(out a, out b);
            Interlocked.Increment(ref result);
            Console.WriteLine(string.Format("({0}/{1}) #{2} : {3}", a, b, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("mm:ss")));
            lock (locker)
            {
                runningThreads--;
                Monitor.Pulse(locker);
            }
        }, null, 500, true);
}

Thread.Sleep(1000);
result = 10;
mre.Set();
lock (locker)
{
    while (runningThreads > 0)
        Monitor.Wait(locker);
}
Console.WriteLine(sw.ElapsedMilliseconds);
Console.WriteLine(result);
Console.ReadLine();      

程式結果如下:

淺談.NET下的多線程和并行計算(五)線程池基礎上

注意到RegisterWaitForSingleObject的第一個參數就是信号量,第二個參數就是方法主體(接受兩個參數分别是傳給線程的一個狀态變量以及線程執行的時候是否逾時),第三個參數是狀态變量,第四個參數逾時時間我們設定了500毫秒,由于主線程在1秒後發出信号,逾時500毫秒,是以這些線程并沒等到信号的發出500毫秒之後就運作了。之是以程式的運作結果為30是因為即使500毫秒之後線程逾時開始執行但是也要等1秒才累加結果,在這個時候主線程早已把結果更新為10了,是以累加從10開始而不是0開始。最後布爾參數為true表明接受到信号後隻線程執行一次。

觀察到,所有線程執行完畢花了7秒的時間,除去開始的等待時間0.5秒,相比之前的例子還多了0.5秒的時間。這是為什麼呢?請大家幫忙分析分析。還有一個更奇怪的問題是,RegisterWaitForSingleObject消耗的是IO線程而不是工作線程,難道微軟覺得RegisterWaitForSingleObject常見于IO操作的應用還是不希望不浪費工作線程?

作者:

lovecindywang

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。