C#多執行緒系列(1):Thread

本篇是《多執行緒入門和實踐(初級)》的第一篇,也是大家相當熟悉和不屑的的最簡單的入門部分。作為系列文章,筆者將從最簡單的部分開始,與各位夥伴一起不斷學習和探究 C# 中的多執行緒。

對於涉及理論的東西,這裡不會過多討論。更加深入的成分會在中級系列加以說明和探討,屆時會有很多與底層相關的知識。

系列文章一般開頭都要寫一些寄語吧?
          那我祝願各位同學要好好學習,天天向上。

學習多執行緒的第一步,就是學習 Thread。Thread 類可以創建和控制執行緒,設置其優先順序並獲取其狀態。這一篇將開始學習執行緒的創建和生命周期。

官方文檔 Thread 類詳細的屬性和方法:

//docs.microsoft.com/zh-cn/dotnet/api/system.threading.thread?view=netcore-3.1#properties

來,打開你的 Visual Studio,一起擼程式碼。

1,獲取當前執行緒資訊

Thread.CurrentThread 是一個 靜態的 Thread 類,Thread 的CurrentThread 屬性,可以獲取到當前運行執行緒的一些資訊,其定義如下:

public static System.Threading.Thread CurrentThread { get; }

Thread 類有很多屬性和方法,這裡就不列舉了,後面的學習會慢慢熟悉更多 API 和深入了解使用。

這裡有一個簡單的示例:

        static void Main(string[] args)
        {
            Thread thread = new Thread(OneTest);
            thread.Name = "Test";
            thread.Start();
            Console.ReadKey();
        }

        public static void OneTest()
        {
            Thread thisTHread = Thread.CurrentThread;
            Console.WriteLine("執行緒標識:" + thisTHread.Name);
            Console.WriteLine("當前地域:" + thisTHread.CurrentCulture.Name);  // 當前地域
            Console.WriteLine("執行緒執行狀態:" + thisTHread.IsAlive);
            Console.WriteLine("是否為後台執行緒:" + thisTHread.IsBackground);
            Console.WriteLine("是否為執行緒池執行緒"+thisTHread.IsThreadPoolThread);
        }

輸出

執行緒標識:Test
當前地域:zh-CN
執行緒執行狀態:True
是否為後台執行緒:False
是否為執行緒池執行緒False

2,管理執行緒狀態

一般認為,執行緒有五種狀態:

新建(new 對象) 、就緒(等待CPU調度)、運行(CPU正在運行)、阻塞(等待阻塞、同步阻塞等)、死亡(對象釋放)。

理論的東西不說太多,直接擼程式碼。

2.1 啟動與參數傳遞

新建執行緒簡直滾瓜爛熟,無非 new 一下,然後 Start()

Thread thread = new Thread(); 

Thread 的構造函數有四個:

public Thread(ParameterizedThreadStart start);

public Thread(ThreadStart start);

public Thread(ParameterizedThreadStart start, int maxStackSize);

public Thread(ThreadStart start, int maxStackSize);

我們以啟動新的執行緒時傳遞參數來舉例,使用這四個構造函數呢?

2.1.1 ParameterizedThreadStart

ParameterizedThreadStart 是一個委託,構造函數傳遞的參數為需要執行的方法,然後在 Start 方法中傳遞參數。

需要注意的是,傳遞的參數類型為 object,而且只能傳遞一個。

程式碼示例如下:

        static void Main(string[] args)
        {
            string myParam = "abcdef";
            ParameterizedThreadStart parameterized = new ParameterizedThreadStart(OneTest);
            Thread thread = new Thread(parameterized);
            thread.Start(myParam);
            Console.ReadKey();
        }

        public static void OneTest(object obj)
        {
            string str = obj as string;
            if (string.IsNullOrEmpty(str))
                return;

            Console.WriteLine("新的執行緒已經啟動");
            Console.WriteLine(str);
        }

2.1.2 使用靜態變數或類成員變數

此種方法不需要作為參數傳遞,各個執行緒共享堆棧。

優點是不需要裝箱拆箱,多執行緒可以共享空間;缺點是變數是大家都可以訪問,此種方式在多執行緒競價時,可能會導致多種問題(可以加鎖解決)。

下面使用兩個變數實現數據傳遞:

    class Program
    {
        private string A = "成員變數";
        public static string B = "靜態變數";

        static void Main(string[] args)
        {
            // 創建一個類
            Program p = new Program();

            Thread thread1 = new Thread(p.OneTest1);
            thread1.Name = "Test1";
            thread1.Start();

            Thread thread2 = new Thread(OneTest2);
            thread2.Name = "Test2";
            thread2.Start();

            Console.ReadKey();
        }

        public void OneTest1()
        {
            Console.WriteLine("新的執行緒已經啟動");
            Console.WriteLine(A);       // 本身對象的其它成員
        }
        public static void OneTest2()
        {
            Console.WriteLine("新的執行緒已經啟動");
            Console.WriteLine(B);       // 全局靜態變數
        }
    }

2.1.3 委託與Lambda

原理是 Thread 的構造函數 public Thread(ThreadStart start);ThreadStart 是一個委託,其定義如下

public delegate void ThreadStart();

使用委託的話,可以這樣寫

        static void Main(string[] args)
        {
            System.Threading.ThreadStart start = DelegateThread;

            Thread thread = new Thread(start);
            thread.Name = "Test";
            thread.Start();


            Console.ReadKey();
        }

        public static void DelegateThread()
        {
            OneTest("a", "b", 666, new Program());
        }
        public static void OneTest(string a, string b, int c, Program p)
        {
            Console.WriteLine("新的執行緒已經啟動");
        }

有那麼一點點麻煩,不過我們可以使用 Lambda 快速實現。

使用 Lambda 示例如下:

        static void Main(string[] args)
        {
            Thread thread = new Thread(() =>
            {
                OneTest("a", "b", 666, new Program());
            });
            thread.Name = "Test";
            thread.Start();
            
            Console.ReadKey();
        }

        public static void OneTest(string a, string b, int c, Program p)
        {
            Console.WriteLine("新的執行緒已經啟動");
        }

提示:如果需要處理的演算法比較簡單的話,可以直接寫進委託中,不需要另外寫方法啦。

可以看到,C# 是多麼的方便。

2.2 暫停與阻塞

Thread.Sleep() 方法可以將當前執行緒掛起一段時間,Thread.Join() 方法可以阻塞當前執行緒一直等待另一個執行緒運行至結束。

在等待執行緒 Sleep()Join() 的過程中,執行緒是阻塞的(Blocket)。

    阻塞的定義:當執行緒由於特點原因暫停執行,那麼它就是阻塞的。

    如果執行緒處於阻塞狀態,執行緒就會交出他的 CPU 時間片,並且不會消耗 CPU 時間,直至阻塞結束。

    阻塞會發生上下文切換。

程式碼示例如下:

        static void Main(string[] args)
        {
            Thread thread = new Thread(OneTest);
            thread.Name = "小弟弟";

            Console.WriteLine($"{DateTime.Now}:大家在吃飯,吃完飯後要帶小弟弟逛街");
            Console.WriteLine("吃完飯了");
            Console.WriteLine($"{DateTime.Now}:小弟弟開始玩遊戲");
            thread.Start();

            // 化妝 5 s
            Console.WriteLine("不管他,大姐姐化妝先"); Thread.Sleep(TimeSpan.FromSeconds(5));

            Console.WriteLine($"{DateTime.Now}:化完妝,等小弟弟打完遊戲");
            thread.Join();

            Console.WriteLine("打完遊戲了嘛?" + (!thread.IsAlive ? "true" : "false"));
            Console.WriteLine($"{DateTime.Now}:走,逛街去");
            Console.ReadKey();
        }

        public static void OneTest()
        {
            Console.WriteLine(Thread.CurrentThread.Name + "開始打遊戲");
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine($"{DateTime.Now}:第幾局:" + i);
                Thread.Sleep(TimeSpan.FromSeconds(2));      // 休眠 2 秒
            }
            Console.WriteLine(Thread.CurrentThread.Name + "打完了");
        }

Join() 也可以實現簡單的執行緒同步,即一個執行緒等待另一個執行緒完成。

2.3 執行緒狀態

ThreadState 是一個枚舉,記錄了執行緒的狀態,我們可以從中判斷執行緒的生命周期和健康情況。

其枚舉如下:

枚舉 說明
Initialized 0 此狀態指示執行緒已初始化但尚未啟動。
Ready 1 此狀態指示執行緒因無可用的處理器而等待使用處理器。 執行緒準備在下一個可用的處理器上運行。
Running 2 此狀態指示執行緒當前正在使用處理器。
Standby 3 此狀態指示執行緒將要使用處理器。 一次只能有一個執行緒處於此狀態。
Terminated 4 此狀態指示執行緒已完成執行並已退出。
Transition 6 此狀態指示執行緒在可以執行前等待處理器之外的資源。 例如,它可能正在等待其執行堆棧從磁碟中分頁。
Unknown 7 執行緒的狀態未知。
Wait 5 此狀態指示執行緒尚未準備好使用處理器,因為它正在等待外圍操作完成或等待資源釋放。 當執行緒就緒後,將對其進行重排。

但是裡面有很多枚舉類型是沒有用處的,我們可以使用一個這樣的方法來獲取更加有用的資訊:

        public static ThreadState GetThreadState(ThreadState ts)
        {
            return ts & (ThreadState.Unstarted |
                ThreadState.WaitSleepJoin |
                ThreadState.Stopped);
        }

此方法來自:《C# 7.0 核心技術指南》第十四章。

根據 2.2 中的示例,我們修改一下 Main 中的方法:

        static void Main(string[] args)
        {
            Thread thread = new Thread(OneTest);
            thread.Name = "小弟弟";

            Console.WriteLine($"{DateTime.Now}:大家在吃飯,吃完飯後要帶小弟弟逛街");
            Console.WriteLine("吃完飯了");
            Console.WriteLine($"{DateTime.Now}:小弟弟開始玩遊戲");
            Console.WriteLine("弟弟在幹嘛?(執行緒狀態):" + Enum.GetName(typeof(ThreadState), GetThreadState(thread.ThreadState)));
            thread.Start();
            Console.WriteLine("弟弟在幹嘛?(執行緒狀態):" + Enum.GetName(typeof(ThreadState), GetThreadState(thread.ThreadState)));
            // 化妝 5 s
            Console.WriteLine("不管他,大姐姐化妝先"); Thread.Sleep(TimeSpan.FromSeconds(5));
            Console.WriteLine("弟弟在幹嘛?(執行緒狀態):" + Enum.GetName(typeof(ThreadState), GetThreadState(thread.ThreadState)));
            Console.WriteLine($"{DateTime.Now}:化完妝,等小弟弟打完遊戲");
            thread.Join();
            Console.WriteLine("弟弟在幹嘛?(執行緒狀態):" + Enum.GetName(typeof(ThreadState), GetThreadState(thread.ThreadState)));
            Console.WriteLine("打完遊戲了嘛?" + (!thread.IsAlive ? "true" : "false"));
            Console.WriteLine($"{DateTime.Now}:走,逛街去");
            Console.ReadKey();
        }

程式碼看著比較亂,請複製到項目中運行一下。

輸出示例:

2020/4/11 11:01:48:大家在吃飯,吃完飯後要帶小弟弟逛街
吃完飯了
2020/4/11 11:01:48:小弟弟開始玩遊戲
弟弟在幹嘛?(執行緒狀態):Unstarted
弟弟在幹嘛?(執行緒狀態):Running
不管他,大姐姐化妝先
小弟弟開始打遊戲
2020/4/11 11:01:48:第幾局:0
2020/4/11 11:01:50:第幾局:1
2020/4/11 11:01:52:第幾局:2
弟弟在幹嘛?(執行緒狀態):WaitSleepJoin
2020/4/11 11:01:53:化完妝,等小弟弟打完遊戲
2020/4/11 11:01:54:第幾局:3
2020/4/11 11:01:56:第幾局:4
2020/4/11 11:01:58:第幾局:5
2020/4/11 11:02:00:第幾局:6
2020/4/11 11:02:02:第幾局:7
2020/4/11 11:02:04:第幾局:8
2020/4/11 11:02:06:第幾局:9
小弟弟打完了
弟弟在幹嘛?(執行緒狀態):Stopped
打完遊戲了嘛?true
2020/4/11 11:02:08:走,逛街去

可以看到 UnstartedWaitSleepJoinRunningStopped四種狀態,即未開始(就緒)、阻塞、運行中、死亡。

2.4 終止

.Abort() 方法不能在 .NET Core 上使用,不然會出現 System.PlatformNotSupportedException:「Thread abort is not supported on this platform.」

後面關於非同步的文章會講解如何實現終止。

由於 .NET Core 不支援,就不理會這兩個方法了。這裡只列出 API,不做示例。

方法 說明
Abort() 在調用此方法的執行緒上引發 ThreadAbortException,以開始終止此執行緒的過程。 調用此方法通常會終止執行緒。
Abort(Object) 引發在其上調用的執行緒中的 ThreadAbortException以開始處理終止執行緒,同時提供有關執行緒終止的異常資訊。 調用此方法通常會終止執行緒。

Abort() 方法給執行緒注入 ThreadAbortException 異常,導致程式被終止。但是不一定可以終止執行緒

2.5 執行緒的不確定性

執行緒的不確定性是指幾個並行運行的執行緒,不確定 CPU 時間片會分配給誰(當然,分配有優先順序)。

對我們來說,多執行緒是同時運行的,但一般 CPU 沒有那麼多核,不可能在同一時刻執行所有的執行緒。CPU 會決定某個時刻將時間片分配給多個執行緒中的一個執行緒,這就出現了 CPU 的時間片分配調度。

執行下面的程式碼示例,你可以看到,兩個執行緒列印的順序是不確定的,而且每次運行結果都不同。

CPU 有一套公式確定下一次時間片分配給誰,但是比較複雜,需要學習電腦組成原理和作業系統。

留著下次寫文章再講。

        static void Main(string[] args)
        {
            Thread thread1 = new Thread(Test1);
            Thread thread2 = new Thread(Test2);

            thread1.Start();
            thread2.Start();

            Console.ReadKey();
        }

        public static void Test1()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("Test1:" + i);
            }
        }
        public static void Test2()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("Test2:" + i);
            }
        }

2.6 執行緒優先順序、前台執行緒和後台執行緒

Thread.Priority 屬性用於設置執行緒的優先順序,Priority 是一個 ThreadPriority 枚舉,其枚舉類型如下

枚舉 說明
AboveNormal 3 可以將 安排在具有 Highest 優先順序的執行緒之後,在具有 Normal 優先順序的執行緒之前。
BelowNormal 1 可以將 Thread 安排在具有 Normal 優先順序的執行緒之後,在具有 Lowest 優先順序的執行緒之前。
Highest 4 可以將 Thread 安排在具有任何其他優先順序的執行緒之前。
Lowest 0 可以將 Thread 安排在具有任何其他優先順序的執行緒之後。
Normal 2 可以將 Thread 安排在具有 AboveNormal 優先順序的執行緒之後,在具有 BelowNormal 優先順序的執行緒之前。 默認情況下,執行緒具有 Normal 優先順序。

優先順序排序:Highest > AboveNormal > Normal > BelowNormal > Lowest

Thread.IsBackgroundThread 可以設置執行緒是否為後台執行緒。

前台執行緒的優先順序大於後台執行緒,並且程式需要等待所有前台執行緒執行完畢後才能關閉;而當程式關閉是,無論後台執行緒是否在執行,都會強制退出。

2.7 自旋和休眠

當執行緒處於進入休眠狀態或解除休眠狀態時,會發生上下文切換,這就帶來了昂貴的消耗。

而執行緒不斷運行,就會消耗 CPU 時間,佔用 CPU 資源。

對於過短的等待,應該使用自旋(spin)方法,避免發生上下文切換;過長的等待應該使執行緒休眠,避免佔用大量 CPU 時間。

我們可以使用最為熟知的 Sleep() 方法休眠執行緒。有很多同步執行緒的類型,也使用了休眠手段等待執行緒(已經寫好草稿啦)。

自旋的意思是,沒事找事做。

例如:

        public static void Test(int n)
        {
            int num = 0;
            for (int i=0;i<n;i++)
            {
                num += 1;
            }
        }

通過做一些簡單的運算,來消耗時間,從而達到等待的目的。

C# 中有關於自旋的自旋鎖和 Thread.SpinWait(); 方法,在後面的執行緒同步分類中會說到自旋鎖。

Thread.SpinWait() 在極少數情況下,避免執行緒使用上下文切換很有用。其定義如下

public static void SpinWait(int iterations);

SpinWait 實質上是(處理器)使用了非常緊密的循環,並使用 iterations 參數指定的循環計數。 SpinWait 等待時間取決於處理器的速度。

SpinWait 無法使你準確控制等待時間,主要是使用一些鎖時用到,例如 Monitor.Enter。

Tags: