關於C#線程,線程池和並行運算的簡單使用和對比

來源:互聯網
上載者:User

前言:看了書上兩個使用C#4.0並行編程的demo,又對照以前收藏的網上幾篇講述線程池的雄文,一併整理,寫個樣本總結一下。寫這篇文章的時候,發現關於線程的好幾個基礎的重要的知識點自己都不熟悉,而且可能習慣性認知淺薄,所以痛苦的無以複加,不知道到底要說什麼。不想看文章的可以直接下載最後的樣本,本文代碼主要參考Marc Clifton的“.NET's ThreadPool Class - Behind The Scenes”,對新手也許有協助。

參考:

http://msdn.microsoft.com/zh-cn/library/system.threading.threadpool(VS.80).aspx

http://www.codeproject.com/KB/threads/threadtests.aspx

http://www.codeproject.com/KB/threads/smartthreadpool.aspx

http://blog.zhaojie.me/2009/07/thread-pool-1-the-goal-and-the-clr-thread-pool.html  (老趙的淺談線程池上中下三篇)

Jeffrey Richter <<CLR via C#>> 3rd Edition

 

先大概看一下控制台應用程式的Main方法的主要代碼:

         static bool done = false;        static decimal count2 = 0;        static int threadDone = 0;//標誌啟用線程數?        static System.Timers.Timer timer = new System.Timers.Timer(1000);        static decimal[] threadPoolCounters = new decimal[10];        static Thread[] threads = new Thread[10];        static System.Timers.Timer[] threadTimers = new System.Timers.Timer[10];        static void Main(string[] args)        {            timer.Stop();            /*當 AutoReset 設定為 false 時,Timer 只在第一個 Interval 過後引發一次 Elapsed 事件。             若要保持以 Interval 時間間隔引發 Elapsed 事件,請將 AutoReset 設定為 true。*/            timer.AutoReset = false;            timer.Elapsed += new ElapsedEventHandler(OnTimerEvent);//當timer.Start()時,觸發事件            decimal total = 0;            // raw test            decimal count1 = SingleThreadTest();//單一線程,一跑到底            Console.WriteLine("Single thread count = " + count1.ToString());            // create one thread, increment counter, destroy thread, repeat            Console.WriteLine();            CreateAndDestroyTest();//建立一個線程,運算,然後銷毀該線程 重複前面的動作            Console.WriteLine("Create and destroy per count = " + count2.ToString());            // Create 10 threads and run them simultaneously            //一次性建立10個線程,然後遍曆使線程執行運算            Console.WriteLine();            InitThreadPoolCounters();            InitThreads();            StartThreads();            while (threadDone != 10) { };            Console.WriteLine("10 simultaneous threads:");            for (int i = 0; i < 10; i++)            {                Console.WriteLine("T" + i.ToString() + " = " + threadPoolCounters[i].ToString() + "   ");                total += threadPoolCounters[i];            }            Console.WriteLine("Total = " + total.ToString());            Console.WriteLine();            Console.WriteLine("///////////////////////////////////////////////////");            // using ThreadPool            //直接通過線程池的QueueUserWorkItem方法,按隊列執行10個任務            Console.WriteLine();            Console.WriteLine("ThreadPool:");            InitThreadPoolCounters();            QueueThreadPoolThreads();            while (threadDone != 10) { };            Console.WriteLine("ThreadPool: 10 simultaneous threads:");            total = 0;            for (int i = 0; i < 10; i++)            {                //threadTimers[i].Stop();                //threadTimers[i].Dispose();                Console.WriteLine("T" + i.ToString() + " = " + threadPoolCounters[i].ToString() + "   ");                total += threadPoolCounters[i];            }            Console.WriteLine("Total = " + total.ToString());            // using SmartThreadPool            //通過Amir Bar的SmartThreadPool線程池,利用QueueUserWorkItem方法,按隊列執行10個任務            Console.WriteLine();            Console.WriteLine("SmartThreadPool:");            InitThreadPoolCounters();            QueueSmartThreadPoolThreads();            while (threadDone != 10) { };            Console.WriteLine("SmartThreadPool: 10 simultaneous threads:");            total = 0;            for (int i = 0; i < 10; i++)            {                Console.WriteLine("T" + i.ToString() + " = " + threadPoolCounters[i].ToString() + "   ");                total += threadPoolCounters[i];            }            Console.WriteLine("Total = " + total.ToString());            // using ManagedThreadPool            //通過Stephen Toub改進後的線程池,利用QueueUserWorkItem方法,按隊列執行10個任務            Console.WriteLine();            Console.WriteLine("ManagedThreadPool:");            InitThreadPoolCounters();            QueueManagedThreadPoolThreads();            while (threadDone != 10) { };            Console.WriteLine("ManagedThreadPool: 10 simultaneous threads:");            total = 0;            for (int i = 0; i < 10; i++)            {                Console.WriteLine("T" + i.ToString() + " = " + threadPoolCounters[i].ToString() + "   ");                total += threadPoolCounters[i];            }            Console.WriteLine("Total = " + total.ToString());            // using C#4.0 Parallel            //通過Tasks.Parallel.For進行並行運算            Console.WriteLine();            Console.WriteLine("Parallel:");            InitThreadPoolCounters();            UseParallelTasks();            while (threadDone != 10) { };            Console.WriteLine("Parallel: 10 simultaneous threads:");            total = 0;            for (int i = 0; i < 10; i++)            {                Console.WriteLine("T" + i.ToString() + " = " + threadPoolCounters[i].ToString() + "   ");                total += threadPoolCounters[i];            }            Console.WriteLine("Total = " + total.ToString());        }

我們可以先熟悉一下大致思路。代碼中,我們主要依靠輸出的數字count或者total來判斷哪個方法執行效率更高(原文是How Hign Can I Count?),通常輸出的數字越大,我們就認為它”乾的活越多“,效率越高。主要實現過程就是通過一個靜態System.Timers.Timer對象的timer執行個體,設定它的Interval屬性和ElapsedEventHandler事件:

        static System.Timers.Timer timer = new System.Timers.Timer(1000);/*當 AutoReset 設定為 false 時,Timer 只在第一個 Interval 過後引發一次 Elapsed 事件。             若要保持以 Interval 時間間隔引發 Elapsed 事件,請將 AutoReset 設定為 true。*/timer.AutoReset = false;timer.Elapsed += new ElapsedEventHandler(OnTimerEvent);//當timer.Start()時,觸發事件

其中,timer的事件觸發的函數:

     static void OnTimerEvent(object src, ElapsedEventArgs e)        {            done = true;        }

每次timer.Start執行的時候,一次測試就將開始,這樣可以確保測試的不同方法都在1000毫秒內跑完。

下面開始具體介紹幾個方法:

A、線程

這個非常簡單,就是通過主線程計算在1000毫秒內,count從0遞增加到了多少:

        /// <summary>        /// 單一線程,一跑到底        /// </summary>        /// <returns></returns>        static decimal SingleThreadTest()        {            done = false;            decimal counter = 0;            timer.Start();            while (!done)            {                ++counter;            }            return counter;        }

while判斷可以保證方法在1000毫秒內執行完成。

 

B、多線程

這個多線程方法比較折騰,先建立線程,然後運行,最後銷毀線程,這就是一個線程執行單元,重複10次這個線程執行單元。

      /// <summary>        /// 建立一個線程,運算,然後銷毀該線程 重複前面的動作        /// </summary>        static void CreateAndDestroyTest()        {            done = false;            timer.Start();            while (!done)            {                Thread counterThread = new Thread(new ThreadStart(Count1Thread));                counterThread.IsBackground = true;//後台線程                counterThread.Start();                while (counterThread.IsAlive) { };            }        }

那個ThreadStart委託對應的方法Count1Thread如下:

        static void Count1Thread()        {            ++count2; //靜態欄位count2自增        }

從表面上看,大家估計都可以猜到,效果可能不佳。

 

C、還是多線程

這個方法不判斷線程的執行狀態,不用等到一個線程銷毀後再建立一個線程,然後執行線程方法。線程執行的方法就是根據線程的Name找到一個指定數組的某一索引,並累加改變數組的值:

    /// <summary>        /// 將數組和線程數標誌threadDone回到初始狀態        /// </summary>        static void InitThreadPoolCounters()        {            threadDone = 0;            for (int i = 0; i < 10; i++)            {                threadPoolCounters[i] = 0;            }        }        /// <summary>        /// 初始化10個線程        /// </summary>        static void InitThreads()        {            for (int i = 0; i < 10; i++)            {                threads[i] = new Thread(new ThreadStart(Count2Thread));                threads[i].IsBackground = true;                threads[i].Name = i.ToString();//將當前線程的Name賦值為數組索引,在Count2Thread方法中擷取對應數組            }        }        /// <summary>        /// 開始多線程運算        /// </summary>        static void StartThreads()        {            done = false;            timer.Start();            for (int i = 0; i < 10; i++)            {                threads[i].Start();            }        }

其中,每一個線程需要執行的委託方法

        static void Count2Thread()        {            int n = Convert.ToInt32(Thread.CurrentThread.Name);//取數組索引            while (!done)            {                ++threadPoolCounters[n];            }            Interlocked.Increment(ref threadDone);//以原子操作的形式保證threadDone遞增        }

在測試過程中,我們看代碼:

      // Create 10 threads and run them simultaneously            //一次性建立10個線程,然後遍曆使線程執行運算            Console.WriteLine();            InitThreadPoolCounters();            InitThreads();            StartThreads();            while (threadDone != 10) { };            Console.WriteLine("10 simultaneous threads:");            for (int i = 0; i < 10; i++)            {                Console.WriteLine("T" + i.ToString() + " = " + threadPoolCounters[i].ToString() + "   ");                total += threadPoolCounters[i];            }            Console.WriteLine("Total = " + total.ToString());            Console.WriteLine();

最後算出這個數組的所有元素的總和,就是這10個線程在1000毫秒內所做的事情。其中, while (threadDone != 10) { };這個判斷非常重要。這個方法看上去沒心沒肺,線程建立好就不管它的死活了(還是管活不管死?),所以效率應該不低。

實際上,我在本地測試並看了一下輸出,表面看來,按count大小逆序排列:C>A>B,這就說明多線程並不一定比單線程運行效率高。其實B之所以效率不佳,主要是由於這個方法大部分的”精力“花線上程的執行狀態和銷毀處理上。

注意,其實C和A、B都沒有可比性,因為C計算的是數組的總和,而A和B只是簡單的對一個數字進行自加。

ps:C這一塊說的沒有中心,想到哪寫到哪,所以看起來寫得很亂,如果看到這裡您還覺著不知所云,建議先下載最後的demo,先看代碼,再對照這篇文章。

好了,到這裡,我們對線程的建立和使用應該有了初步的瞭解。細心的人可能會發現,我們new一個Thread,然後給線程執行個體設定屬性,比如是否後台線程等等,其實這部分工作可以交給下面介紹的線程池ThreadPool來做(D、E和F主要介紹線程池)。

 

D、線程池ThreadPool

在實際的項目中大家可能使用最多最熟悉的就是這個類了,所以沒什麼可說的:

        /// <summary>        /// ThreadPool測試        /// </summary>        static void QueueThreadPoolThreads()        {            done = false;            for (int i = 0; i < 10; i++)            {                ThreadPool.QueueUserWorkItem(new WaitCallback(Count3Thread), i);            }            timer.Start();        }        static void Count3Thread(object state)        {            int n = (int)state;            while (!done)            {                ++threadPoolCounters[n];            }            Interlocked.Increment(ref threadDone);        }

我們知道線程池裡的線程預設都是後台線程,所以它實際上簡化了線程的屬性設定,更方便非同步編程。

需要說明的是,線程池使用過程中會有這樣那樣的缺陷(雖然本文的幾個線程池任務都不會受這種缺陷影響)。比如,我們一次性向線程池中加入100個任務,但是當前的系統可能只支援25個線程,並且每個線程正處於”忙碌“狀態,如果一次性加入池中系統會處理不過來,那麼多餘的任務必須等待,這就造成等待的時間過長,系統無法響應。還好,ThreadPool提供了GetAvailableThreads方法,可以讓你知道當前可用的背景工作執行緒數量。

        static void QueueThreadPoolThreads()        {            done = false;            for (int i = 0; i < 10; i++)            {                //ThreadPool.QueueUserWorkItem(new WaitCallback(Count3Thread), i); //直接給程式池新增工作有時是很草率的                WaitCallback wcb = new WaitCallback(Count3Thread);                int workerThreads, availabeThreads;                ThreadPool.GetAvailableThreads(out workerThreads, out availabeThreads);                if (workerThreads > 0)//可用線程數>0                {                    ThreadPool.QueueUserWorkItem(wcb, i);                }                else                {                    //to do 可以採取一種策略,讓這個任務合理地分配給線程                }            }

如果沒有可用的背景工作執行緒數,必須設計一定的策略,讓這個任務合理地分配給線程。

也許就是類似於上面那樣的限制,很多開發人員都自己建立自己的線程池,同時也就有了後面的SmartThreadPool和ManagedThreadPool大展身手的機會。

 

E、線程池SmartThreadPool

大名鼎鼎的SmartThreadPool,但是我從來沒在項目中使用過,所以只是找了一段簡單的代碼測試一下:

        /// <summary>        /// SmartThreadPool測試        /// </summary>        static void QueueSmartThreadPoolThreads()        {            SmartThreadPool smartThreadPool = new SmartThreadPool();            // Create a work items group that processes             // one work item at a time            IWorkItemsGroup wig = smartThreadPool.CreateWorkItemsGroup(1);            done = false;            timer.Start();            for (int i = 0; i < 10; i++)            {                wig.QueueWorkItem(new WorkItemCallback(Count4Thread), i);            }            // Wait for the completion of all work items in the work items group            wig.WaitForIdle();            smartThreadPool.Shutdown();        }       static object Count4Thread(object state)        {            int n = (int)state;            while (!done)            {                ++threadPoolCounters[n];            }            Interlocked.Increment(ref threadDone);            return null;        }

自從收藏這個SmartThreadPool.dll後,我還從沒有在項目中使用過。查看它的源碼注釋挺少也挺亂的,不知道有沒有高人知道它的一個效率更好的方法。您也可以看看英文原文,自己嘗試體驗一下。如果您熟悉使用SmartThreadPool,歡迎討論。

 

F、線程池ManagedThreadPool

Stephen Toub這個完全用C#Managed 程式碼實現的線程池也非常有名,在Marc Clifton的英文原文中,作者也不吝溢美之詞,贊它“quite excellent”,用當前異軍突起的一個詞彙形容就是太給力了,於我心有戚戚焉:

     /// <summary>        /// ManagedThreadPool測試        /// </summary>        static void QueueManagedThreadPoolThreads()        {            done = false;            timer.Start();            for (int i = 0; i < 10; i++)            {                Toub.Threading.ManagedThreadPool.QueueUserWorkItem(new WaitCallback(Count5Thread), i);            }        }        static void Count5Thread(object state)        {            int n = (int)state;            while (!done)            {                ++threadPoolCounters[n];            }            Interlocked.Increment(ref threadDone);        }

 

對於這個託管的線程池,我個人的理解,就是它在管理線程的時候,這個池裡還有一個緩衝線程的池,即一個ArrayList對象。它一開始就初始化了一定數量的線程,並通過ProcessQueuedItems方法保證非同步執行進入池中的隊列任務(那個死迴圈有時可能導致CPU過分忙碌),這樣在分配非同步任務的時候,就省去了頻繁去建立(new)一個線程。同時它在實現訊號量(Semaphore)的同步和線程出入隊列的設計上都可圈可點,非常巧妙,強烈推薦您閱讀它的源碼。

 

G、並行運算

下面的樣本,我只使用了簡單的System.Threading.Tasks.Parallel.For 對應的for 迴圈的並行運算:

        /// <summary>        /// 並行運算測試        /// </summary>        static void UseParallelTasks()        {            done = false;            timer.Start();            // System.Threading.Tasks.Parallel.For - for 迴圈的並行運算            System.Threading.Tasks.Parallel.For(0, 10, (i) => { Count6Thread(i); });        }        static void Count6Thread(object state)        {            int n = (int)state;            while (!done)            {                ++threadPoolCounters[n];            }            Interlocked.Increment(ref threadDone);        }

沒有什麼要特殊說明的,就是新類庫的使用。看代碼,好像比使用線程或線程池更加簡單直接,有機會爭取多用一用。我在本地測試的時候,在Release版本下,按照count的大小逆序排列,總體上G>D>F>E。需要注意到一件事,就是SmartThreadPool中排入隊列的任務是一個傳回值為Object的委託類型,這和其他的幾個沒有返回的(void類型)不同。SmartThreadPool口碑還是不錯的,也許是我沒有正確使用它。

 

最後小結一下:本文主要列舉了C#中我所知道的幾種常見的非同步處理的方法,歡迎大家錯誤修正或補充。

 

樣本下載:demo

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.