本文主要參考:
https://www.cnblogs.com/qtiger/p/13497807.html
ThreadPool中有若干數量的線程。當有任務需要處理時,會從線程池中獲取一個空閑的線程來執行任務,任務執行完畢后線程不會銷毀,而是被線程池回收以供后續任務使用。當線程池中所有的線程都被占用,又有新任務要處理時,線程池會新建一個線程來處理該任務。如果線程數量達到設置的最大值,任務會排隊,等待其他任務釋放線程后再執行。ThreadPool相對於Thread來說可以減少線程的創建,有效減小系統開銷。但是ThreadPool不能控制線程的執行順序,也不能獲取線程池內線程取消/異常/完成的通知,即不能有效監控和控制線程池中的線程。因此NET4.0在ThreadPool的基礎上推出了Task。Task擁有線程池的優點,同時也解決了使用線程池不易控制的弊端。
1.無返回值的Task的創建和執行
using System; using System.Threading.Tasks; using System.Threading; namespace TaskDemo { class Program { static void Main(string[] args) { // 實例化一個Task,通過Start方法啟動 Task task = new Task( () => { Thread.Sleep(1000); Console.WriteLine($"NEW實例化一個task,線程ID為{Thread.CurrentThread.ManagedThreadId}"); } ); task.Start(); // Task.Factory.StartNew(Action action)創建和啟動一個Task Task task2 = Task.Factory.StartNew( () => { Thread.Sleep(500); Console.WriteLine($"Task.Factory.StartNew方式創建一個task,線程ID為{Thread.CurrentThread.ManagedThreadId}"); }); // Task.Run(Action action)將任務放在線程池隊列,返回並啟動一個Task Task task3 = Task.Run( () => { Thread.Sleep(200); Console.WriteLine($"Task.Run方式創建一個task,線程ID為{Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine("執行主線程"); Console.Read(); } } }
運行結果:
2.用Task.Result獲取返回值的Task的創建和執行
namespace TaskDemo { class Program { static void Main(string[] args) { // 有返回值的啟動task Task<string> task = new Task<string>( () => { Thread.Sleep(1000); return $"NEW實例化一個task,線程ID為{Thread.CurrentThread.ManagedThreadId}"; } ); task.Start(); // Task.Factory.StartNew(Action action)創建和啟動一個Task Task<string> task2 = Task.Factory.StartNew( () => { Thread.Sleep(3000); return $"Task.Factory.StartNew方式創建一個task,線程ID為{Thread.CurrentThread.ManagedThreadId}"; }); // Task.Run(Action action)將任務放在線程池隊列,返回並啟動一個Task Task<string> task3 = Task.Run( () => { Thread.Sleep(2000); return $"Task.Run方式創建一個task,線程ID為{Thread.CurrentThread.ManagedThreadId}"; }); Console.WriteLine("執行主線程"); Console.WriteLine(task.Result); Console.WriteLine(task2.Result); Console.WriteLine(task3.Result); Console.Read(); } } }
運行結果:
可見Task.Result獲取返回值時會阻塞線程。本例中,必須等到task2執行完成,獲取到返回值后,才能繼續執行task3。但是上面兩個例子中的Task的執行都是異步的,不會阻塞主線程。
3.同步執行Task,會阻塞主線程
namespace TaskDemo { class Program { static void Main(string[] args) { Task task = new Task( () => { Thread.Sleep(1000); Console.WriteLine("執行Task結束"); } ); // 同步執行,會阻礙主線程 task.RunSynchronously(); Console.WriteLine("執行主線程"); Console.Read(); } } }
運行結果:
4.Task的阻塞方法(Wait/WaitAll/WaitAny)
4.1Thread阻塞線程的方法
使用thread.Join()方法可阻塞主線程
namespace TaskDemo { class Program { static void Main(string[] args) { Thread thread1 = new Thread( () => { Thread.Sleep(1000); Console.WriteLine("線程1執行完畢"); }); thread1.Start(); Thread thread2 = new Thread( () => { Thread.Sleep(2000); Console.WriteLine("線程2執行完畢"); }); thread2.Start(); //阻塞主線程 thread1.Join(); thread2.Join(); Console.WriteLine("主線程執行完畢"); Console.Read(); } } }
運行結果:
使用Thread.Join()方法的弊端包括:
- 如果要實現很多線程的阻塞,每個線程都要調用一次Join()方法;
- 如果讓所有的線程執行完畢(或任一線程執行完畢)時,立即解除阻塞,使用Join()方法不容易實現。
4.2使用Task Wait/WaitAll/WaitAny方法,實現阻塞線程
- task.Wait()表示等待task執行完畢,類似於thread.Join()
- task.WaitAll(Task[] tasks)表示只有所有的task都執行完畢再解除阻塞
- task.WaitAny(Task[] tasks)表示只要有一個task執行完畢就解除阻塞
Task task1 = new Task( () => { Thread.Sleep(1000); Console.WriteLine("線程1執行完畢"); }); task1.Start(); Task task2 = new Task( () => { Thread.Sleep(2000); Console.WriteLine("線程2執行完畢"); }); task2.Start(); // 阻塞主線程。task1和task2都執行完畢再執行主線程 //task1.Wait(); //task2.Wait(); Task.WaitAll(new Task[] { task1, task2 }); Console.WriteLine("主線程執行完畢"); Console.Read();
運行結果:
使用task1.Wait(); task2.Wait()可以達到同樣的目的。如果把WaitAll改成WaitAny,則運行結果如下所示:
5.Task的延續操作(WhenAny/WhenAll/ContinueWith)
Wait/WaitAll/WaitAny方法返回值都是void,這些方法只是單純的實現阻塞線程。使用WhenAny/WhenAll/ContinueWith方法可以讓task執行完畢后,繼續執行后續操作,這些方法執行完成返回一個task實例。
- task.WhenAll(Task[] tasks)表示所有的task都執行完畢后再去執行后續的操作
- task.WhenAny(Task[] tasks)表示任一task執行完畢后就開始執行后續操作
Task task1 = new Task( () => { Thread.Sleep(1000); Console.WriteLine("線程1執行完畢"); }); task1.Start(); Task task2 = new Task( () => { Thread.Sleep(2000); Console.WriteLine("線程2執行完畢"); }); task2.Start(); Task.WhenAll(new Task[] { task1, task2 }).ContinueWith( (t) => { Thread.Sleep(1000); Console.WriteLine("執行后續操作完畢"); }); Console.WriteLine("主線程執行完畢"); Console.Read();
運行結果:
WhenAll/WhenAny方法並不會阻塞主線程。也可以使用Task.Factory.ContinueWhenAll來實現
Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) => { Thread.Sleep(1000); Console.WriteLine("執行后續操作完畢"); });
6.Task的任務取消(CancellationTokenSource)
6.1Thread取消任務執行
通過設置一個變量來控制任務是否停止。
bool isStop = false; int index = 0; Thread thread1 = new Thread( () => { Console.WriteLine($"thread1的線程ID是{Thread.CurrentThread.ManagedThreadId}"); while (!isStop) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次執行,線程運行中..."); } }); thread1.Start(); Console.WriteLine($"主線程開始執行,主線程的ID是{Thread.CurrentThread.ManagedThreadId}"); // 5s后取消任務執行 Thread.Sleep(5000); isStop = true; Console.WriteLine("主線程執行完畢"); Console.Read();
運行結果:
6.2Task取消任務執行
使用專門類CancellationTokenSource來取消任務執行。
CancellationTokenSource source = new CancellationTokenSource(); int index = 0; Task task1 = new Task( () => { while (!source.IsCancellationRequested) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次執行,線程運行中..."); } }); task1.Start(); Console.WriteLine("主線程開始執行"); Thread.Sleep(5000); source.Cancel(); Console.WriteLine("主線程執行完畢"); Console.Read();
運行結果:
還可以使用source.CancelAfter(5000)實現5s后自動取消任務,即Thread.Sleep(5000); source.Cancel();這兩條代碼由source.CancelAfter(5000)取代。運行結果:
注意這兩次運行結果中,“主線程執行完畢”的區別。也可以通過source.Token.Register(Action action)注冊取消任務觸發的回調函數。
CancellationTokenSource source = new CancellationTokenSource(); source.Token.Register( () => { Console.WriteLine("任務被取消后執行的操作"); }); int index = 0; Task task1 = new Task( () => { Console.WriteLine($"task1的線程ID是{Thread.CurrentThread.ManagedThreadId}"); while (!source.IsCancellationRequested) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次執行,線程運行中..."); } }); task1.Start(); Console.WriteLine($"主線程開始執行,主線程的ID是{Thread.CurrentThread.ManagedThreadId}"); source.CancelAfter(5000); Console.WriteLine("主線程執行完畢"); Console.Read();
運行結果:
7.異步方法(async/await)
async static Task<string>GetContentAsync(string fileName) { Console.WriteLine($"當前線程ID是{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"開始讀取文件:{DateTime.Now}"); Thread.Sleep(1000); using(StreamReader sr = new StreamReader(fileName)) { string program = await sr.ReadToEndAsync(); Console.WriteLine($"讀取文件結束:{DateTime.Now}"); return program; } } // 同步讀取文件內容 static string GetContent(string fileName) { using (StreamReader sr = new StreamReader(fileName)) { string program = sr.ReadToEnd(); return program; } } static void Main(string[] args) { string path = @"D:\Demos\TaskDemo\postdata.txt"; Console.WriteLine($"主線程ID是{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"主程序執行開始:{DateTime.Now}"); string content = GetContentAsync(path).Result; Console.WriteLine($"主程序輸入結果:{content}"); Console.WriteLine($"主程序執行結束:{DateTime.Now}"); Console.Read(); }
運行結果:
主程序等待GetContentAsync方法執行完畢后,獲取到返回值后才繼續執行。這說明,如果調用方法要從調用中獲取一個T類型的值,異步方法的返回類型必須是Task<T>,而且調用會獲取到返回值后才會繼續執行下去。如果僅僅是調用一下異步方法,不和異步方法做其他交互,則將異步方法簽名返回值為void,這種調用形式也被稱為“調用並忘記”。
async static void GetContentAsync(string fileName) { Console.WriteLine($"當前線程ID是{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"開始讀取文件:{DateTime.Now}"); Thread.Sleep(1000); using(StreamReader sr = new StreamReader(fileName)) { string program = await sr.ReadToEndAsync(); Console.WriteLine($"讀取文件結束:{DateTime.Now}"); } }
static void Main(string[] args) { string path = @"D:\Demos\TaskDemo\postdata.txt"; Console.WriteLine($"主線程ID是{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"主程序執行開始:{DateTime.Now}"); GetContentAsync(path); Console.WriteLine($"主程序執行結束:{DateTime.Now}"); Console.Read(); }
運行結果: