首頁 > 軟體

C#非同步和多執行緒以及Thread、ThreadPool、Task區別和使用方法

2021-02-07 21:30:18

本文的目的是為了讓大家瞭解什麼是非同步?什麼是多執行緒?如何實現多執行緒?對於當前C#當中三種實現多執行緒的方法如何實現和使用?什麼情景下選用哪一技術更好?

第一部分主要介紹在C#中非同步(async/await)和多執行緒的區別,以及async/await使用方法。

第二部分主要介紹在C#多執行緒當中Thread、ThreadPool、Task區別和使用方法。

-------------------------------------------------------------------------------------------------------------------------

 async/await這裡的非同步只是一種程式設計模式,一個程式設計介面設計為非同步的,大多數時候都是為了靈活地處理並行流程需求的,對於async/await用法請看以下程式碼:

static void Main(string[] args)
        {
            _ = Async1();
            Console.WriteLine("...............按任意鍵退出");
            Console.ReadKey();
        }
       
        static async Task Async1()
        {
            Console.WriteLine("非同步開始");
            var r = await Async2();
            var x = await Async3(r);
            Console.WriteLine("結果是 {0}", r + x);
        }

        static async Task<int> Async2()
        {
            await Task.Delay(1000);//一種非同步延遲方法
            return 100;
        }

        static async Task<int> Async3(int x)
        {
            await Task.Delay(1000);
            return x % 7;
        }

執行結果:

 

使用async關鍵字修飾的方法為非同步方法,async關鍵字要和await關鍵字一同使用才會生效。通過這個程式執行結果我們可以看到對於async/await方法的非同步是在遇到await關鍵字時開始的,如果你編寫的程式碼中只用到了async關鍵字修飾方法,但是沒有用到await關鍵字,那麼此方法執行起來與普通方法一樣都是順序執行的。

使用async/await方法可以實現非同步,但我個人覺得從程式碼閱讀的難易程度上來說,使用async/await關鍵字的程式碼更難以閱讀,我更推薦使用Task來實現非同步,後續會詳細介紹Task。

使用async修飾的方法返回值有三種型別void,Task,Task<T>,根據返回值型別我認為其實async/await的實現是基於Task的(個人的理解我並沒有在任何書籍或者官方資料中看到這樣的說法,歡迎交流),說完了async/await非同步程式設計模式再來說一下在C#中三個多執行緒實現非同步的方法的方法Thread,ThreadPool,Task。

 

按照他們在C#中釋出的順序先來說一下Thread,使用Thread實現以上的功能程式碼要如何編寫呢?我們看一下範例:

static void Main(string[] args)
        {
            Thread thread = new Thread(Fun1);
            //Thread thread = new Thread(() => Fun1(0)); 多執行緒呼叫時有引數傳遞的寫法
            Console.WriteLine("非同步開始");
            //thread.IsBackground = true; Thread預設是前臺執行緒,IsBackground = true設定為後臺執行緒
            thread.Start();

            Console.WriteLine("...............按任意鍵退出");
            Console.ReadKey();
        }

        static void Fun1()
        {
            var r = Fun2();
            var x = Fun3(r);
            Console.WriteLine("結果是 {0}", r + x);
        }

        static int Fun2()
        {
            Thread.Sleep(1000);
            return 100;
        }

        static int Fun3(int x)
        {
            Thread.Sleep(1000);
            return x % 7;
        }

執行結果:

 

Thread的使用方法如上,新建一個執行緒會有一定的記憶體消耗(執行緒什麼都不做的情況下大約消耗1M)也需要一定的時間,Thread預設是前臺執行緒,前臺執行緒就是當程式主執行緒結束時會等待前臺執行緒結束返回後主執行緒才結束,後臺執行緒是當主執行緒結束時後臺執行緒直接結束,主執行緒不會等待後臺執行緒結束。當呼叫start方法時才開始執行Thread多執行緒方法。對於Thread多執行緒引數的傳遞方法一,首先引數的型別必須是object,其次通過Start方法傳遞引數。方法二,我更推薦通過以上程式碼中註釋的寫法通過Lambda表示式實現。

終止執行緒方法:t.Abort(); 此方法是通過向t執行緒中丟擲異常的方式強制終止執行緒,我們可以線上程中捕獲此異常(ThreadAbortException)系統在finally 子句的結尾處會再次引發ThreadAbortException 異常,如果沒有finally 子句,則會在Catch 子句的結尾處再次引發該異常。為了避免再次引發異常,可以在finally 子句的結尾處或者Catch 子句的結尾處呼叫System.Threading.Thread.ResetAbort 方法防止系統再次引發該異常。注:此方法不支援.Net Core 3.0,不知道為啥各種終止執行緒的方法在.Net Core 3.0都不支援,可能是個坑。

合併執行緒方法:t2.Join(); Join 方法用於把兩個並行執行的執行緒合併為一個單個的執行緒。如果一個執行緒t1 在執行的過程中需要等待另一個執行緒t2 結束後才能繼續執行,可以在t1 的程式模組中呼叫t2 的join()方法。這樣t1 在執行到t2.Join()語句後就會處於阻塞狀態,直到t2 結束後才會繼續執行。但是假如t2 一直不結束,那麼等待就沒有意義了。為了解決這個問題,可以在呼叫t2 的Join 方法的時候指定一個等待時間,這樣t1 這個執行緒就不會一直等待下去了。例如,如果希望將t2 合併到t1 後,t1 只等待100 毫秒,然後不論t2 是否結束,t1 都繼續執行,就可以在t1中加上語句:t2.Join(100)。注:貌似這個join方法也不支援.Net Core 3.0。

 

接下來介紹一下ThreadPool執行緒池,就像我上面介紹Thread新建執行緒是需要消耗一定的時間和記憶體的。舉個例子如果把執行緒比作小汽車那麼new Thread就好比是造一輛新車拿來用,而ThreadPool就好比是一個租車行,需要用車可以去租一個,用完還給租車行當有其它人來租車繼續租出去。這樣就節省了頻繁new Thread造車的開支。基於以上ThreadPool的特性我們不難看出來對於執行緒池的特性適合於需要頻繁新建執行緒並且每個執行緒使用的時間較短的場景,例如C/S模式使用者端存取伺服器端。執行緒池使用方法範例如下:

static void Main(string[] args)
        {
            Console.WriteLine("主執行緒執行!");

            ThreadPool.SetMinThreads(1, 1);//設定執行緒池最小執行緒數
            ThreadPool.SetMaxThreads(5, 5);//設定執行緒池最大執行緒數
            //引數一:執行緒池按需建立的最小工作執行緒數。引數二:執行緒池按需建立的最小非同步I/O執行緒數。

            for (int i = 1; i <= 10; i++)
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback(testFun), i);
            }

            Console.WriteLine("主執行緒結束!");

            Console.WriteLine("...............按任意鍵退出");
            Console.ReadKey();
        }
        public static void testFun(object obj)
        {
            Console.WriteLine(string.Format("{0}:第{1}個執行緒", DateTime.Now.ToString(), obj.ToString()));
            Thread.Sleep(5000);
        }

執行結果:

 

由以上的程式中可以看出ThreadPool執行緒池是一個靜態類。執行緒池可以看做容納執行緒的容器;一個應用程式最多隻能有一個執行緒池;ThreadPool靜態類通過QueueUserWorkItem()方法將工作函數排入執行緒池; 每排入一個工作函數,就相當於請求建立一個執行緒;

執行緒池是為突然大量爆發的執行緒設計的,通過有限的幾個固定執行緒為大量的操作服務,減少了建立和銷燬執行緒所需的時間,從而提高效率。如果一個執行緒的時間非常長,就沒必要用執行緒池了(不是不能作長時間操作,而是不宜。),況且我們還不能控制執行緒池中執行緒的開始、掛起、和中止。

執行緒池這樣的使用還有一個缺點就是我們無法得知執行緒在什麼時候結束,我們可以使用AutoResetEvent類的WaitOne()方法和Set()方法來獲得執行緒池中執行緒的執行和返回情況,此方法用於執行緒同步在此就不詳細展開介紹了哈。

 

最後一個Task,也是我個人比較推薦的,使用方法如下:

static void Main(string[] args)
        {
            Console.WriteLine("主執行緒執行!");

            //方法一
            Task t1 = new Task(() =>
            {
                Console.WriteLine("方法1的任務開始工作……");
                Thread.Sleep(5000);
                Console.WriteLine("方法1的任務工作完成……");
            });
            t1.Start();
            //方法二
            Task.Run(() => 
            {
                Console.WriteLine("方法2的任務開始工作……");
                Thread.Sleep(5000);
                Console.WriteLine("方法2的任務工作完成……");
            });
            //方法三
            var t3 = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("方法3的任務開始工作……");
                Thread.Sleep(5000);
                Console.WriteLine("方法3的任務工作完成……");
            });

            Console.WriteLine("主執行緒結束!");

            Console.WriteLine("...............按任意鍵退出");
            Console.ReadKey();
        }

執行結果:

 

以上是三種使用Task多執行緒的方法,Task是基於執行緒池封裝實現的,解決了執行緒池無法掛起中止執行緒等這些問題。而且Task的效能優於ThreadPool因為它使用的不是執行緒池的全域性佇列,而是使用的是本地佇列。使得執行緒之間競爭資源的情況減少。Task提供了豐富的API,開發者可對Task進行多種管理,控制。對於「Task t1 = new Task(() =>」和「var t3 = Task.Factory.StartNew(() =>」有什麼區別,區別並不大後者在呼叫時是可以傳入更多引數,設定執行緒的執行時間等(關於這一部分的詳細介紹可以閱讀博文結尾參照的文章C#Task詳解)。

帶返回值的Task使用方法:

static void Main(string[] args)
        {
            Console.WriteLine("主執行緒執行!");

            Task<int> task = CreateTask("Task 1");
            task.Start();
            int result = task.Result;
            Console.WriteLine("Task 1 Result is: {0}", result);
            Console.WriteLine("主執行緒結束!");

            Console.WriteLine("...............按任意鍵退出");
            Console.ReadKey();
        }

        static Task<int> CreateTask(string name)
        {
            return new Task<int>(() => TaskMethod(name));
        }
        static int TaskMethod(string name)
        {
            Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
                name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
            Thread.Sleep(TimeSpan.FromSeconds(2));
            return 42;
        }

執行結果:

 

 以上是帶有返回值Task的用法。

接下來介紹以下在Task中常用的一些管理和控制方法:

 ContinueWith()方法,在Task執行緒執行完成後執行,程式碼如下:

static void Main(string[] args)
        {
            Console.WriteLine("主執行緒執行!");

            Task t1 = new Task(() =>
            {
                Console.WriteLine("方法1的任務開始工作……");
                Thread.Sleep(5000);
                Console.WriteLine("方法1的任務工作完成……");
            });
            t1.Start();

            t1.ContinueWith(t => 
            {
                Console.WriteLine("方法1的任務工作完成了!");
            });
            Console.WriteLine("主執行緒結束!");

            Console.WriteLine("...............按任意鍵退出");
            Console.ReadKey();
        }

執行結果:

 

Task.WaitAll(t1, t2);等待t1和t2 Task執行緒完成,此方法可以傳入若干個Tsak執行緒。會阻塞當前執行緒,程式碼如下:

static void Main(string[] args)
        {
            Console.WriteLine("主執行緒執行!");

            Task t1 = new Task(() =>
            {
                Console.WriteLine("方法1的任務開始工作……");
                Thread.Sleep(5000);
                Console.WriteLine("方法1的任務工作完成……");
            });
            t1.Start();

            Task t2 = new Task(() =>
            {
                Console.WriteLine("方法2的任務開始工作……");
                Thread.Sleep(5000);
                Console.WriteLine("方法2的任務工作完成……");
            });
            t2.Start();

            Task.WaitAll(t1, t2);
            
            Console.WriteLine("主執行緒結束!");

            Console.WriteLine("...............按任意鍵退出");
            Console.ReadKey();
        }

執行結果:

 

基於以上的兩個方法可以實現執行緒中的同步和管理等,如果以上的方法不能滿足你的開發需要,那需要請你對於某一項單獨的類進行更加深入的學習和了解可以瀏覽以下部落格,以下部落格均是我在整理和學習這部分知識時有所收穫的部落格,本文在有些段落和例子也參照於以下博文。

清華大學出版社《C#從入門到精通(第3版)》

淺析C#中的Thread ThreadPool Task和async/await

談談C#的非同步和多執行緒

C#多執行緒與非同步的區別

c#的async到不是不是非同步,它和多執行緒是什麼關係

C#執行緒Thread類

C#多執行緒--執行緒池(ThreadPool)

C#Task詳解

總結:多執行緒是一種實現非同步的一種方法,在多執行緒中三個常用的方法,如果是執行緒要長時間執行的建議使用Thread,如果需要很多執行緒並行並且執行緒執行時間較短建議使用ThreadPool,其它的一般情況選擇效率相對較高的Task。

以上博文有任何錯漏歡迎指正交流。


IT145.com E-mail:sddin#qq.com