執行緒(一)——執行緒,執行緒池,Task概念+程式碼實踐

JerryMouseLi發表於2020-12-14

執行緒(一)——執行緒,執行緒池,Task概念+程式碼實踐

摘要

執行緒中的概念很多,如果沒有程式碼示例來理解,會比較晦澀,而且有些概念落不到實處,因此,本文以一些執行示例程式碼,結果來闡述執行緒中的一些基礎概念。讓自己跟讀者一起把執行緒中的概念理解地更深刻。

1 執行緒安全

1.1 未出現執行緒搶佔

    class ThreadTest2
    {
        bool done;

        static void Main()
        {
            ThreadTest2 tt = new ThreadTest2();   // Create a common instance
            new Thread(tt.Go).Start();
            tt.Go();
        }
                // Note that Go is now an instance method
        void Go()
        {
                if (!done)
                {
                    done = true;
                    Console.WriteLine("Done");             
                }            
        }
    }

執行結果如下:

Done

1.2 執行緒搶佔

    class ThreadTest2
    {
        bool done;

        static void Main()
        {
            ThreadTest2 tt = new ThreadTest2();   // Create a common instance
            new Thread(tt.Go).Start();
            tt.Go();
        }
                // Note that Go is now an instance method
        void Go()
        {
                if (!done)
                {                   
                    Console.WriteLine("Done");        
                    done = true;     
                }            
        }
    }

執行結果如下:

Done
Done

執行緒搶佔例子2:

for (int i = 0; i < 10; i++)
  new Thread (() => Console.Write (i)).Start();

執行結果
0223557799

1.3 避免執行緒搶佔

    class ThreadTest2
    {
        static readonly object locker = new object();
        bool done;

        static void Main()
        {
            ThreadTest2 tt = new ThreadTest2();   // Create a common instance
            new Thread(tt.Go).Start();
            tt.Go();
        }

        // Note that Go is now an instance method
        void Go()
        {
            lock (locker)
            {
                if (!done)
                {                  
                  Console.WriteLine("Done");
                  done = true;
                }
            }
        }
    }

執行結果如下:

Done

2 執行緒阻塞

    class Program
    {
        static void Main()
        {
            Thread t = new Thread(Go);
            t.Start();
            t.Join();
            Console.WriteLine("Thread t has ended!");
        }

        static void Go()
        {
            for (int i = 0; i < 1000; i++) Console.Write("y");
        }
    }

執行結果:

1000個y列印完畢才輸出"Thread t has ended!"。

Thread.Sleep (500);
也會阻塞執行緒,讓渡CPU的執行權給其他執行緒。

3 Thread.yield()和Thread.sleep(0)

sleep(0)效果相當於yield(),會讓當前執行緒放棄剩餘時間片,進入相同優先順序執行緒佇列的隊尾,只有排在前面的所有同優先順序執行緒完成排程後,它才能再次獲執行的機會。

4 執行緒如何工作

多線痛通過內部的執行緒排程器(thread scheduler)管理,通過clr委託作業系統。執行緒排程器會分配適當的執行時間給活動執行緒,執行緒等待(鎖)或者執行緒阻塞(使用者輸入)不會消耗cpu執行時間。
單核處理器電腦上,在Windows,時間片通常會被分配幾十毫秒,遠大於執行緒上下文切換還時間幾毫秒。
在多處理器計算機上,多執行緒是通過時間片和真正的併發實現的,其中不同的執行緒在不同的CPU上同時執行程式碼。 幾乎可以肯定,由於作業系統需要服務自己的執行緒以及其他應用程式的執行緒,因此還會有一定的時間片。
當執行緒的執行由於諸如時間片之類的外部因素而被中斷時,該執行緒被認為是被搶佔的。 在大多數情況下,執行緒無法控制其被搶佔的時間和地點。

5 執行緒與程式

執行緒與程式有相似之處。 就像程式在計算機上並行執行一樣,多個執行緒在單個程式中並行執行。 程式彼此完全隔離; 執行緒的隔離度有限。 特別是,執行緒與在同一應用程式中執行的其他執行緒共享(堆)記憶體。 這就是為什麼執行緒有用的原因:例如,一個執行緒可以在後臺獲取資料,而另一個執行緒可以在資料到達時顯示資料。

6 執行緒的使用和濫用

  • 利於響應式使用者介面
    在同時並行執行的“worker”執行緒上執行耗時的任務,主UI執行緒可以自由繼續處理鍵盤和滑鼠事件。

  • 有效利用原本被阻塞的CPU
    當執行緒正在等待來自另一臺計算機或硬體的響應時,多執行緒很有用。 當一個執行緒在執行任務時被阻塞時,其他執行緒可以利用本來沒有負擔的計算機的其他執行緒來響應任務。

  • 並行程式設計
    如果以``分而治之''策略在多個執行緒之間共享工作負載,則執行密集計算的程式碼可以在多核或多處理器計算機上更快地執行(請參閱第5部分)。

  • 隨機執行
    在多核計算機上,有時可以通過預測可能需要完成的事情然後提前進行來提高效能。 LINQPad使用此技術來加速新查詢的建立。 一種變化是並行執行許多不同的演算法,這些演算法都可以解決同一任務。 誰先獲得“勝利”,當您不知道哪種演算法執行速度最快時,此方法將非常有效。

  • 允許服務同時處理請求
    在伺服器上,客戶端請求可以同時到達,因此需要並行處理(如果使用ASP.NET,WCF,Web服務或遠端處理,.NET Framework會為此自動建立執行緒)。 這在客戶端上也很有用(例如,處理對等網路-甚至來自使用者的多個請求)。

使用ASP.NET和WCF之類的技術,您如果不知道多執行緒正在發生-除非您在沒有適當鎖定的情況下訪問共享資料(可能通過靜態欄位),會破壞執行緒安全性。

執行緒之間的互動(通常是通過共享資料),會帶來很多複雜性,但卻不可避免,因此,有必要將互動保持在最低限度,並儘可能地堅持簡單可靠的設計。

好的策略是將多執行緒邏輯封裝到可重用的類中,這些類可以獨立檢查和測試。 框架本身提供了許多更高階別的執行緒結構,我們將在後面介紹。

執行緒化還會在排程和切換執行緒時(如果活動執行緒多於CPU核心)會導致資源和CPU的浪費,並且還會產生建立/釋放成本。 多執行緒並不總是可以加快您的應用程式的速度-如果使用過多或使用不當,它甚至可能減慢其速度。 例如,當涉及大量磁碟I / O時,讓幾個工作執行緒按順序執行任務比一次執行10個執行緒快得多。

7 執行緒傳參

7.1 lambda表示式傳參

最方便的方法就是通過lambda表示式呼叫匿名方法,傳引數。

        static void Main()
        {
            Thread t = new Thread(() =>Print("Hello from t!"));
            t.Start();
        }

        static void Print(string message)
        {
            Console.WriteLine(message);
        }

7.2 執行緒start方法傳參

        static void Main()
        {
            Thread t = new Thread(Print);
            t.Start("Hello from t!");
        }

        static void Print(object messageObj)
        {
            string message = (string)messageObj;   // We need to cast here
            Console.WriteLine(message);
        }

7.3 執行緒建立需要時間

string text = "t1";
Thread t1 = new Thread ( () => Console.WriteLine (text) );
 
text = "t2";
Thread t2 = new Thread ( () => Console.WriteLine (text) );
 
t1.Start();
t2.Start();

執行結果:

t2
t2

以上執行結果說明,在t1執行緒建立之前text被修改成了t2。

8 執行緒命名

每個執行緒都有名稱屬性,目的是為了更方便除錯。

            static void Main()
            {
                Thread.CurrentThread.Name = "main";
                Thread worker = new Thread(Go);
                worker.Name = "worker";
                worker.Start();
                Go();
            }

            static void Go()
            {
                Console.WriteLine("Hello from " + Thread.CurrentThread.Name);
            }

執行結果:

Hello from main
Hello from worker

9 前臺執行緒與後臺執行緒

            Thread worker = new Thread(() => Console.ReadLine());
            if (args.Length > 0) worker.IsBackground = true;
            worker.Name = "backThread";
            worker.Start();
            Console.WriteLine("finish!");

前臺執行緒會隨著主執行緒視窗關閉而停止,後臺執行緒及時主執行緒視窗關閉自己獨立執行。

10 執行緒優先順序

執行緒優先順序決定了作業系統執行活動執行緒時間的長短。

enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }

有時候提高了執行緒的優先順序,但卻仍然無法滿足一些實時的應用需求,這時候就需要提高程式的優先順序,System.Diagnostics名稱空間中的process程式類.

using (Process p = Process.GetCurrentProcess())
  p.PriorityClass = ProcessPriorityClass.High;

實際上,ProcessPriorityClass.High比最高優先順序低1個級別:Realtime。 將程式優先順序設定為Realtime,可指示OS,您永遠不希望該程式將CPU時間浪費在另一個程式上。 如果您的程式進入意外的無限迴圈,您甚至可能會發現作業系統已鎖定,只剩下電源按鈕可以拯救您! 因此,高通常是實時應用程式的最佳選擇。

如果您的實時應用程式具有使用者介面,則提高處理優先順序將給螢幕更新帶來過多的CPU時間,從而減慢整個計算機的速度(尤其是在UI複雜的情況下)。 降低主執行緒的優先順序並提高程式的優先順序可確保實時執行緒不會因螢幕重繪而被搶佔,但不會解決使其他應用程式耗盡CPU時間的問題,因為作業系統仍會分配 整個過程的資源不成比例。 理想的解決方案是使實時工作程式和使用者介面作為具有不同程式優先順序的單獨應用程式執行,並通過遠端處理或記憶體對映檔案進行通訊。 記憶體對映檔案非常適合此任務。 我們將在C#4.0的第14和25章中簡要介紹它們的工作原理。

11 異常處理

Go無法補捉異常,GoCatch能捕獲當前執行緒的異常,輸出Console.WriteLine("exception.");由此可見,執行緒建立之後,異常只能由本執行緒捕獲,如果其呼叫方需要捕獲,則得用共享記憶體方式往上傳,Task幫我們做了這件事,呼叫方可在task.result裡捕獲到其他執行緒的異常。

        public static void Main()
        {
            try
            {
                new Thread(Go).Start();
                Console.ReadKey();
            }
            catch (Exception ex)
            {
                // We'll never get here!
                Console.WriteLine("Exception!");
            }
        }

        static void Go() { throw null; }   // Throws a NullReferenceException

        static void GoCatch()
        {
            try
            {
                // ...
                throw null;    // The NullReferenceException will get caught below
                               // ...
            }
            catch (Exception ex)
            {
                // Typically log the exception, and/or signal another thread
                // that we've come unstuck
                // ...
                Console.WriteLine("exception.");
            }
        }

12 執行緒池

當你建立一個執行緒,幾百毫秒會被花費在例如建立本地私有變數堆疊。每個執行緒都會預設消耗1MB記憶體,從而允許在非常精細的級別上應用多執行緒而不會影響效能。當利用多核處理器以“分而治之”的方式並行執行計算密集型程式碼時,這很有用。
執行緒池還限制了將同時執行的工作執行緒總數。活動執行緒過多會限制作業系統的管理負擔,並使CPU快取無效。一旦達到限制,作業將排隊並僅在另一個作業完成時才開始。這使任意併發的應用程式成為可能,例如Web伺服器。 (非同步方法模式是一種先進的技術,通過高效利用池執行緒來進一步實現這一點;我們在C#4.0的第23章中簡要介紹了這一點)。
有多種進入執行緒池的方法:
•通過Task Parallel Library(來自Framework 4.0)
•通過呼叫ThreadPool.QueueUserWorkItem
•通過非同步委託(await)
•通過BackgroundWorker

以下方法間接使用執行緒池:
•WCF,遠端,ASP.NET和ASMX Web服務應用程式伺服器
•System.Timers.Timer和System.Threading.Timer
•以Async結尾的框架方法,例如WebClient(基於事件的非同步模式)上的框架方法和大多數BeginXXX方法(非同步程式設計模型模式)
•PLINQ

使用池執行緒時,需要注意以下幾點:
•您無法設定池執行緒的名稱,這會使除錯更加困難(儘管您可以在Visual Studio的“執行緒”視窗中進行除錯時附加說明)。
•池執行緒始終是後臺執行緒(這通常不是問題)。
•除非您呼叫ThreadPool.SetMinThreads(請參閱優化執行緒池),否則阻塞執行緒池可能會在應用程式的早期階段觸發額外的延遲。
您可以自由更改池執行緒的優先順序-將其釋放回池後將恢復為正常狀態。

您可以通過Thread.CurrentThread.IsThreadPoolThread屬性查詢當前是否線上程池上執行。

12.1 通過TPL進入執行緒池

通過Task Parallel Library庫中的Task類可輕鬆使用執行緒池,Task類由Framework 4.0引入,如果你熟悉老的結構,考慮用不帶泛型Task類來替代ThreadPool.QueueUserWorkItem,而泛型Task 代表的是一個非同步委託。 新的結構更快,更方便,比老的更靈活。

使用不帶泛型例子的Task類,呼叫Task.Factory.StartNew,傳遞一個目標方法的委託;

        static void Main()    // The Task class is in System.Threading.Tasks
        {
            var task=Task.Factory.StartNew(Go);
            Console.WriteLine("main");

            task.Wait() ;
            Console.WriteLine(task.Result);
            Console.ReadLine();
        }
        static string Go()
        {
            if (Thread.CurrentThread.IsThreadPoolThread)
            { Console.WriteLine("Hello from the thread pool!"); }
            else { Console.WriteLine("Hello just from the thread!"); }
            return "task complete!";
        }

輸出結果:

main
Hello from the thread pool!
task complete!

12.1.1 Task異常捕獲

        static void Main()    // The Task class is in System.Threading.Tasks
        {
            var task=Task.Factory.StartNew(Go);
            Console.WriteLine("main");
            try
            { task.Wait(); }                                   
             catch (Exception e)
            {
                Console.WriteLine("exception!");
            }
            Console.WriteLine(task.Result);
            Console.ReadLine();
        }
        static string Go()
        {
            if (Thread.CurrentThread.IsThreadPoolThread)
            { Console.WriteLine("Hello from the thread pool!"); }
            else { Console.WriteLine("Hello just from the thread!"); }
            throw null;
            return "task complete!";
        }

執行結果,在主執行緒中捕獲到了其他執行緒的異常:

static void Main()
{
  // Start the task executing:
  Task<string> task = Task.Factory.StartNew<string>
    ( () => DownloadString ("http://www.linqpad.net") );
 
  // We can do other work here and it will execute in parallel:
  RunSomeOtherMethod();
 
  // When we need the task's return value, we query its Result property:
  // If it's still executing, the current thread will now block (wait)
  // until the task finishes:
  string result = task.Result;
}
 
static string DownloadString (string uri)
{
  using (var wc = new System.Net.WebClient())
    return wc.DownloadString (uri);
}

Task<string> 就是一個返回值為string的非同步委託。

12.2 不同過TPL進入執行緒池

如果你的框架是.Net 4.0之前的,你可以不通過Task Parallel Library 進入執行緒池。

12.2.1 QueueUserWorkItem

        static void Main()
        {
            ThreadPool.QueueUserWorkItem(Go);
            ThreadPool.QueueUserWorkItem(Go, 123);
            Console.ReadLine();
        }
        static void Go(object data)   // data will be null with the first call.
        {
            Console.WriteLine("Hello from the thread pool! " + data);
        }

執行結果:

Hello from the thread pool!
Hello from the thread pool! 123

與Task不同:

  • 後續執行中無法返回執行結果;
  • 無法返回異常給呼叫者;

12.2.2 非同步委託

即鄙人寫的這篇文章深入理解C#中的非同步(一)——APM模式EAP模式裡的2.1APM非同步程式設計模式。
需要補充說明的是:
委託的EndInvoke 做了3件事:

  • 阻塞等待;
  • 返回結果;
  • 向呼叫者跑出異常;

2.1.3 為非同步委託的回撥例子

12.3 執行緒池優化

執行緒池從其池中的一個執行緒開始。 分配任務後,池管理器會“注入”新執行緒以應對額外的併發工作負載(最大限制)。 在足夠長時間的不活動之後,如果池管理器懷疑這樣做會導致更好的吞吐量,則可以“退出”執行緒。
您可以通過呼叫ThreadPool.SetMaxThreads;來設定池將建立的執行緒的上限; 預設值為:
•32位環境中的Framework 4.0中的1023
•64位環境中的Framework 4.0中的32768
•Framework 3.5中的每個核心250個
•Framework 2.0中每個核心25個

您還可以通過呼叫ThreadPool.SetMinThreads設定下限。 下限的作用是微妙的:這是一種高階優化技術,它指示池管理器在達到下限之前不要延遲執行緒的分配。 當存在阻塞的執行緒時,提高最小執行緒數可提高併發性。
預設的下限是每個處理器核心一個執行緒-允許全部CPU利用率的最小值。 但是,在伺服器環境(例如IIS下的ASP .NET)上,下限通常要高得多-多達50個或更多。
設定執行緒池最小執行緒數量。

ThreadPool.SetMinThreads (50, 50);

13 程式碼

本文程式碼git下載

14 參考文章

Threading in C#


版權宣告:本文為博主翻譯文章+自己理解,部分程式碼自己寫,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連結和本宣告。 本文連結:https://www.cnblogs.com/JerryMouseLi/p/14135600.html

相關文章