C#多線程系列(1):Thread

本篇是《多線程入門和實踐(初級)》的第一篇,也是你們至關熟悉和不屑的的最簡單的入門部分。做爲系列文章,筆者將從最簡單的部分開始,與各位夥伴一塊兒不斷學習和探究 C# 中的多線程。算法

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

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

學習多線程的第一步,就是學習 Thread。Thread 類能夠建立和控制線程,設置其優先級並獲取其狀態。這一篇將開始學習線程的建立和生命週期。api

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

https://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 和深刻了解使用。this

這裏有一個簡單的示例:操作系統

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。
相關文章
相關標籤/搜索