C#異步和多線程以及Thread、ThreadPool、Task區別和使用方法


本文的目的是為了讓大家了解什么是異步?什么是多線程?如何實現多線程?對於當前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。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM