c# Thread、ThreadPool、Task有什麼區別,什麼時候用,以及Task的使用

c# Thread、ThreadPool、Task有什麼區別,什麼時候用,以及Task的使用

這三者都是爲了處理耗時任務,且都是異步的。

Thread

Thread就是Thread,需要自己調度,適合長跑型的操作。

ThreadPoll

ThreadPool是Thread基礎上的一個線程池,目的是減少頻繁創建線程的開銷。線程很貴,要開新的stack,要增加CPU上下文切換,所以ThreadPool適合頻繁、短期執行的小操作。調度算法是自適應的,會根據程序執行的模式調整配置,通常不需要自己調度線程。另外分爲Worker和IO兩個池。IO線程對應Native的overlapped io,Win下利用IO完成端口實現非阻塞IO。

【Thread vs. ThreadPoll】

前臺線程:主程序必須等待線程執行完畢後纔可退出程序。Thread默認爲前臺線程,也可以設置爲後臺線程。

後臺線程:主程序執行完畢後就退出,不管線程是否執行完畢。ThreadPool默認爲後臺線程。

線程消耗:開啓一個新線程,線程不做任何操作,都要消耗1M左右的內存。

總結:ThreadPoll 性能優於 Thread,但是 Thread 和 ThreadPoll 對線程的控制都不是很好,例如線程等待(線程執行一段時間無響應後,直接停止線程),釋放資源等,都沒有直接的API來控制,只能通過硬編碼來實現。同時,ThreadPool 使用的是線程池全局隊列,全局隊列中的線程依舊會存在競爭共享資源的情況,從而影響性能。

task

Task 是在 .NET Framework 4 中添加進來的。這是新的 namespace:System.Threading.Tasks;它強調的是 adding parallelism and concurrency to applications。在語法上,和 lamda 表達式更好地結合。

Task 背後的實現也是使用了線程池線程,但它的性能優於ThreadPoll,因爲它使用的不是線程池的全局隊列,而是本地隊列,使線程之間的資源競爭減少。同時,Task 提供了豐富的 API 來管理、控制線程。但是相對前面兩種耗內存,Task 依賴於 CPU,對於多核的CPU性能遠超前兩者,而對於單核的CPU,三者性能沒什麼差別。

創建Task有兩種方法:

//工廠創建,直接執行
Task t = Task.Factory.StartNew(() => {

                Console.WriteLine("任務已啓動....");

            });

或者

//直接實例化,必須手動去Start
Task t2 = new Task(() => {

                Console.WriteLine("開啓一個新任務");

            });

            t2.Start();//任務已啓動...

第一種方法不需要調用start,初始化後任務就開始了。

Task還可以隨時取消正在執行的任務,請看下面的代碼

static void Main(string[] args) {
        CancellationTokenSource cts = new CancellationTokenSource();            
        Task t3 = new Task(() => LongRunTask(cts.Token));            
        t2.Start();            
        Thread.Sleep(3000);            
        cts.Cancel();            
        Console.Read();
}

private static void LongRunTask(CancellationToken token) {            
    while (true) {                
        if (!token.IsCancellationRequested) {                    
            Thread.Sleep(500);                    
            Console.WriteLine(".");                
        } else {                    
            Console.WriteLine("任務取消了");                    
            break;                
        }            
    }        
}

Task的任務控制:Task最吸引人的地方就是他的任務控制了,你可以很好的控制task的執行順序,讓多個task有序的工作

方法名 說明
Task.Wait task1.Wait(); 就是等待任務執行(task1)完成,task1的狀態變爲Completed
Task.WaitAll 待所有的任務都執行完成
Task.WaitAny 同Task.WaitAll,就是等待任何一個任務完成就繼續向下執行
Task.ContinueWith 第一個Task完成後自動啓動下一個Task,實現Task的延續
CancellationTokenSource 通過cancellation的tokens來取消一個Task

使用 Task 代替 ThreadPool 和 Thread

【Task 的優勢】

ThreadPool 相比Thread來說具備了很多優勢,但是ThreadPool卻又存在一些使用上的不方便。比如:

1. ThreadPool 不支持線程的取消、完成、失敗通知等交互性操作;

2. ThreadPool 不支持線程執行的先後次序。

以往,如果開發者要實現上述功能,需要完成很多額外的工作,現在,FCL中提供了一個功能更強大的概念:Task。Task在線程池的基礎上進行了優化,並提供了更多的API。在 FCL4.0 中,如果我們要編寫多線程程序,Task顯然已經優於傳統的方式。

以下是一個簡單的任務示例:

static void Main(string[] args)
{
    Task t = new Task(() =>
    {
        Console.WriteLine("任務開始工作……");
        //模擬工作過程
        Thread.Sleep(5000);
    });
    t.Start();
    t.ContinueWith((task) =>
    {
        Console.WriteLine("任務完成,完成時候的狀態爲:");
        Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}",
                        task.IsCanceled, task.IsCompleted, task.IsFaulted);                                                        
    });
    Console.ReadKey();
}

【Task的完成狀態】

任務 Task 有這樣一些屬性,讓我們查詢任務完成時的狀態:

1: IsCanceled,因爲被取消而完成;

2: IsCompleted,成功完成;

3: IsFaulted,因爲發生異常而完成。

需要注意的是,任務並沒有提供回調事件來通知完成(像 BackgroundWorker 一樣),它通過啓用一個新任務的方式來完成類似的功能。ContinueWith 方法可以在一個任務完成的時候發起一個新任務,這種方式天然就支持了任務的完成通知:可以在新任務中獲取原任務的結果值。

下面是一個稍微複雜一點的例子,同時支持完成通知、取消、獲取任務返回值等功能:

static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            Task<int> t = new Task<int>(() => Add(cts.Token), cts.Token);
            t.Start();
            t.ContinueWith(TaskEnded);
            //等待按下任意一個鍵取消任務
            Console.ReadKey();
            cts.Cancel();
            Console.ReadKey();
        }

        static void TaskEnded(Task<int> task)
        {
            Console.WriteLine("任務完成,完成時候的狀態爲:");
            Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", 
                            task.IsCanceled, task.IsCompleted, task.IsFaulted);
            Console.WriteLine("任務的返回值爲:{0}", task.Result);
        }

        static int Add(CancellationToken ct)
        {
            Console.WriteLine("任務開始……");
            int result = 0;
            while (!ct.IsCancellationRequested)
            {
                result++;
                Thread.Sleep(1000);
            }
            return result;
        }

在任務開始後大概3秒鐘的時候按下鍵盤,會得到如下的輸出:

任務開始……
任務完成,完成時候的狀態爲:
IsCanceled=False IsCompleted=True IsFaulted=False
任務的返回值爲:3

你也許會奇怪,我們的任務是通過Cancel的方式處理,爲什麼完成的狀態IsCanceled那一欄還是False。這是因爲在工作任務中,我們對於IsCancellationRequested進行了業務邏輯上的處理,並沒有通過ThrowIfCancellationRequested方法進行處理。如果採用後者的方式,如下:

static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            Task<int> t =new Task<int>(() => AddCancleByThrow(cts.Token), cts.Token);
            t.Start();
            t.ContinueWith(TaskEndedByCatch);
            //等待按下任意一個鍵取消任務
            Console.ReadKey();
            cts.Cancel();
            Console.ReadKey();
        }

        static void TaskEndedByCatch(Task<int> task)
        {
            Console.WriteLine("任務完成,完成時候的狀態爲:");
            Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", 
                            task.IsCanceled, task.IsCompleted, task.IsFaulted);
            try
            {
                Console.WriteLine("任務的返回值爲:{0}", task.Result);
            }
            catch (AggregateException e)
            {
                e.Handle((err) => err is OperationCanceledException);
            }
        }

        static int AddCancleByThrow(CancellationToken ct)
        {
            Console.WriteLine("任務開始……");
            int result = 0;
            while (true)
            {
                ct.ThrowIfCancellationRequested();
                result++;
                Thread.Sleep(1000);
            }
            return result;
        }

那麼輸出爲:

任務開始……
任務完成,完成時候的狀態爲:
IsCanceled=True  IsCompleted=True  IsFaulted=False

在任務結束求值的方法TaskEndedByCatch中,如果任務是通過 ThrowIfCancellationRequested 方法結束的,對任務求結果值將會拋出異常 OperationCanceledException,而不是得到拋出異常前的結果值。這意味着任務是通過異常的方式被取消掉的,所以可以注意到上面代碼的輸出中,狀態 IsCancled 爲True。

再一次,我們注意到取消是通過異常的方式實現的,而表示任務中發生了異常的IsFaulted狀態卻還是等於False。這是因爲 ThrowIfCancellationRequested 是協作式取消方式類型 CancellationTokenSource 的一個方法,CLR進行了特殊的處理。CLR知道這一行程序開發者有意爲之的代碼,所以不把它看作是一個異常(它被理解爲取消)。要得到 IsFaulted 等於 True 的狀態,我們可以修改 While 循環,模擬一個異常出來:

while (true)
            {
                //ct.ThrowIfCancellationRequested();
                if (result == 5)
                {
                    thrownew Exception("error");
                }
                result++;
                Thread.Sleep(1000);
            }

模擬異常後的輸出爲:

任務開始……
任務完成,完成時候的狀態爲:
IsCanceled=False  IsCompleted=True  IsFaulted=True

【任務工廠】

Task還支持任務工廠的概念。任務工廠支持多個任務之間共享相同的狀態,如取消類型CancellationTokenSource就是可以被共享的。通過使用任務工廠,可以同時取消一組任務:

static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            //等待按下任意一個鍵取消任務
            TaskFactory taskFactory = new TaskFactory();
            Task[] tasks =new Task[]
                {
                    taskFactory.StartNew(() => Add(cts.Token)),
                    taskFactory.StartNew(() => Add(cts.Token)),
                    taskFactory.StartNew(() => Add(cts.Token))
                };
            //CancellationToken.None指示TasksEnded不能被取消
            taskFactory.ContinueWhenAll(tasks, TasksEnded, CancellationToken.None);
            Console.ReadKey();
            cts.Cancel();
            Console.ReadKey();
        }
        
        static void TasksEnded(Task[] tasks)
        {
            Console.WriteLine("所有任務已完成!");
        }

以上代碼輸出爲:

任務開始……
任務開始……
任務開始……
所有任務已完成(取消)!

上面演示了Task(任務)和TaskFactory(任務工廠)的使用方法。Task甚至進一步優化了後臺線程池的調度,加快了線程的處理速度。在FCL4.0時代,使用多線程,我們理應更多地使用Task。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章