線程(Thread、ThreadPool)
線程的定義我想大家都有所了解,這里我就不再復述了。我這里主要介紹.NET Framework中的線程(Thread、ThreadPool)。
.NET Framework中的線程分為兩類:1.前台線程;2.后台線程。
1.前台線程

class Program { static void Main(string[] args) { Console.WriteLine("=====Thread====="); TestThread(); Console.WriteLine("主線程執行完畢"); } public static void TestThread() { Thread thread = new Thread(PrintNum); thread.Start(); } public static void PrintNum() { Thread.Sleep(3000); for (int i = 0; i < 10; i++) Console.WriteLine(i); } }
運行結果
從運行結果可以看出,主線程雖然執行完畢了,但是並沒有退出程序,而是等待子線程執行完畢后,退出程序。
2.后台線程

class Program { static void Main(string[] args) { Console.WriteLine("=====ThreadPool====="); ThreadPool.QueueUserWorkItem(new WaitCallback(PrintNum)); Console.WriteLine("主線程執行完畢"); } public static void PrintNum(object obj) { Thread.Sleep(3000); for (int i = 0; i < 10; i++) Console.WriteLine(i); } }
運行結果
從運行結果可以看出,主線程運行完畢后,就直接退出了程序,沒有等待子線程。
總結:
1.前台線程:主線程執行完畢后,會等待所有子線程執行完畢后,才退出程序。
2.后台線程:主線程執行完畢后,直接退出程序,不論子線程是否執行完畢。
3.推薦:多線程的操作,推薦使用線程池線程而非新建線程。因為就算只是單純的新建一個線程,這個線程什么事情也不做,都大約需要1M的內存空間來存儲執行上下文數據結構,並且線程的創建與回收也需要消耗資源,耗費時間。而線程池的優勢在於線程池中的線程是根據需要創建與銷毀,是最優的存在。但是這也有個問題,那就是線程池線程都是后台線程,主線程執行完畢后,不會等待后台線程而直接結束程序。所以下面就要引出.NET Framework4.0提供的Task,來解決此類問題。
Task
Task是.NET Framework4.0提供的新的操作線程池線程的封裝類。它提供:等待、終止、返回值...優化線程操作的功能。
1.定義
Task 對象是一種的中心思想 基於任務的異步編程模式 首次引入.NET Framework 4 中。 因為由執行工作 Task 對象通常上異步執行一個線程池線程而不是以同步方式在主應用程序線程中,您可以使用 Status 屬性,以及 IsCanceled, ,IsCompleted, ,和 IsFaulted 屬性,以此來確定任務的狀態。
以上MSDN中對Task的定義,從“異步執行一個線程池線程”可以得出Task的后台實現是通過線程池線程實現。
2.補充
Task的性能要優於ThreadPool。
1)ThreadPool的代碼將以先進先出的算法存儲在全局隊列中,並且多個工作者線程之間競爭同一個同步鎖。(這就Task性能優於ThreadPool的第一個原因)
2)Task的代碼將以先進后出的算法存儲在本地隊列中,工作者線程執行本地隊列中的代碼沒有同步鎖的限制(這是Task性能優於ThreadPool的第二個原因),並且當工作者線程2空閑並且工作者線程1忙碌時,工作者線程2會嘗試從工作者線程1(或者別的忙碌的工作者線程)的本地隊列尾部“偷”任務,並會獲取一個同步鎖,不過這種行為很少發生。
3)簡單調用

class Program { static void Main(string[] args) { TestSimpleTask(); } public static void TestSimpleTask() { Console.WriteLine("=====Task====="); //直接創建 Task task2 = new Task(() => { Thread.Sleep(3000); for (int i = 0; i < 10; i++) { Console.WriteLine(i); } }); //如果你想測試超時等待后,任務是否會繼續執行。就替換下面的代碼 task2.Start(); task2.Wait(); Console.WriteLine("主程序執行完畢"); /*測試超時 task2.Start(); task2.Wait(1000); Console.WriteLine("主程序執行完畢"); Console.ReadLine(); */ } }
task.Wati(時間);這個方法可以確定等待任務的執行時間,當超過規定的時間后將不再等待,直接運行之后的代碼,但是任務的代碼仍然在后台運行,如果想超過等待時間就停止任務的執行,你需要看下文深入學習。(這個方法提供了極大的方便。如果在.NET Framework2.0中,實現類似的功能,需要定義一個時間全局變量,然后在線程中不斷的循環判斷。)
3.深入學習
Task(Action, CancellationToken, TaskCreationOptions)
以上是MSDN中Task(不包含輸入參數與返回值)最復雜的構造函數,包含2個重要的參數CancellationToken、TaskCreationOptions,下面將詳細介紹CancellationToken、TaskCreationOptions的意義以及運用。
a.CancellationToken(取消標記)
該類用來檢測任務是否被取消。需要與System.Threading.CancellationTokenSource配合使用,CancellationTokenSource主動停止任務。(CancellationToken雖然有檢測任務是否停止的屬性,但是一旦CancellationTokenSource調用了Cancel()方法,那么任務將立即停止運行,也就是說任務中的任何代碼都不會被執行)
場景:主線程拋出異常,在異常處理中停止任務的執行。

class Program { static void Main(string[] args) { TestCancellationTokenTask(); } public static void TestCancellationTokenTask() { CancellationTokenSource cts = new CancellationTokenSource(); try { Task task = Task.Factory.StartNew(() => { for (int i = 0; i < 10; i++) { //當任務取消時,這段檢測代碼將永遠不會被執行,因為任務已經被取消了 if (cts.Token.IsCancellationRequested) { Console.WriteLine("=====Task====="); Console.WriteLine("任務被取消"); break; } else { Console.WriteLine("=====Task====="); Console.WriteLine("子線程打印:{0}", i); Thread.Sleep(1000); } } }, cts.Token); for (int i = 0; i < 5; i++) { if (i == 3) { Console.WriteLine("=====Main====="); Console.WriteLine("主線程拋出異常"); throw new Exception("測試"); } Console.WriteLine("=====Main====="); Console.WriteLine("主線程打印:{0}", i); Thread.Sleep(1000); } task.Wait(); } catch { cts.Cancel(); } Console.WriteLine(cts.IsCancellationRequested); } }
注意:主線程拋出異常,無論任務是否被顯示取消,都會停止運行。
b.TaskCreationOptions(任務創建選項)
以下是MSDN中關於TaskCreationOptions的枚舉值,具體的運用還是要根據實際情況。下面介紹一下AttachedToParent的用法(第5、第6,實際的運用還需要多參考大神的運用)
// 默認
1.None
// 將任務放入全局隊列中(任務將以先到先出的原則被執行)
2.PreferFairness
// 告訴TaskScheduler,線程可能要“長時間運行”
3.LongRunning
// 將一個Task與它的父Task關聯
4.AttachedToParent
// Task以分離的子任務執行
5.DenyChildAttach
// 創建任務的執行操作將被視為TaskScheduler.Default默認計划程序
6.HideScheduler
// 強制異步執行添加到當前任務的延續任務
7.RunContinuationsAsynchronously
場景:將多個任務關聯為父子任務

class Program { static void Main(string[] args) { TestTaskCreationOptionsTask(); } public static void TestTaskCreationOptionsTask() { StringBuilder sb = new StringBuilder(); Task parent = new Task(() => { new Task(() => { sb.Append("任務1"); }).Start(); new Task(() => { sb.Append("任務2"); }).Start(); new Task(() => { sb.Append("任務3"); }).Start(); //parent任務的調用線程停止5s,這樣“任務1”、“任務2”、“任務3”就有時間執行完畢了。 //這里用來測試,當任務彼此之間是獨立的,那么只有這種方式,控制台才會有打印。 //Thread.Sleep(5000); }); /* Task parent = new Task(() => { new Task(() => { sb.Append("任務1"); }, TaskCreationOptions.AttachedToParent).Start(); new Task(() => { Thread.Sleep(3000); sb.Append("任務2"); }, TaskCreationOptions.AttachedToParent).Start(); new Task(() => { Thread.Sleep(3000); sb.Append("任務3"); }, TaskCreationOptions.AttachedToParent).Start(); }); */ parent.Start(); parent.Wait(); Console.WriteLine(sb.ToString()); } }
說明:
1.Task的創建如果沒有TaskCreationOptions.AttachedToParent,那么任務彼此之間是獨立的,parent任務不會等待“任務1”、“任務2”、“任務3”都執行完畢后,才認為已經結束。
2.Task的創建如果有TaskCreationOptions.AttachedToParent,那么父任務必須等待子任務都執行完畢后,才會認為任務結束。
注意:雖然“任務1”、“任務2”、“任務3”是在parent任務中創建,但是可以分配在不同的線程池本地隊列中,由不同的線程調用並且任務間並不是串行執行,而是並行執行。
c.返回值
以上介紹的所有內容,Task都沒有返回值,但是在實際運用中,Task執行完之后,返回一個值供外部代碼使用,這種情況很常見。

class Program { static void Main(string[] args) { TestReturnValueTask(); } public static void TestReturnValueTask() { Task<int> task = new Task<int>(num => { Thread.Sleep(5000); return (int)num + 1; }, 100); task.Start(); Console.WriteLine(task.Result); } }
注意:當Task的返回值被調用時,主線程會等待Task執行完畢,才會退出程序。所以這里沒有調用task.Wait();
d.TaskContinuationOptions(任務延續選項)
有時候,我們需要在一個任務結束后,執行另一個任務。兩個任務之間有先后順序,這個時候,就需要用到TaskContinuationOptions。
以下是MSDN中關於TaskContinuationOptions的枚舉值,這里只列出了部分(4.5新增的枚舉只根據MSDN的機器翻譯確實不太理解如何運用,還是需要花些時間測試,這里就不列出了,怕誤導讀者)
// 默認
1.None
// 將任務放入全局隊列中(任務將以先到先出的原則被執行)
2.PreferFairness
// 告訴TaskScheduler,線程可能要“長時間運行”,需要為任務創建一個專用線程,而不是排隊讓線程池線程來處理
3.LongRunning
// 將一個Task與它的父Task關聯
4.AttachedToParent
// 希望執行第一個Task的線程,執行ContinueWith任務
5.ExecuteSynchronously
// 第一個任務沒有完成,執行后續任務
6.NotOnRanToCompletion
// 第一個任務沒有失敗,執行后續任務
7.NotOnFaulted
// 第一個任務沒有取消,執行后續任務
8.NotOnCanceled
// 只有當第一個任務取消,執行后續任務
9.OnlyOnCanceled
// 只有當第一個任務失敗,執行后續任務
10.OnlyOnFaulted
// 只有當第一個任務完成,執行后續任務
11.OnlyOnRanToCompletion

class Program { static void Main(string[] args) { TestContinueTask(); } public static void TestContinueTask() { /* Task<int> task = new Task<int>(num => { return (int)num + 1; }, 100); Task taskContinue = task.ContinueWith(c => { Console.WriteLine(c.Result); }, TaskContinuationOptions.OnlyOnRanToCompletion); task.Start(); taskContinue.Wait(); */ CancellationTokenSource cts = new CancellationTokenSource(); Task<int> task = new Task<int>(num => { return (int)num + 1; }, 100, cts.Token); Task taskContinue = task.ContinueWith(c => { Console.WriteLine("任務Task被取消了"); }, TaskContinuationOptions.OnlyOnCanceled); task.Start(); cts.Cancel(); taskContinue.Wait(); } }
寫到這里,關於Task的介紹已經結束。但是Task的內容還有很多:異常處理(AggregateException)、取消通知委托(CancellationToken的Register方法)這些內容就留給讀者自己去學習了。
有時候,可能需要一次性創建多個任務,並且這些任務共享相同的狀態。那么我們就可以通過任務工廠來創建。
TaskFactory
任務工廠,顧名思義,用來創建任務的工廠。在大多數情況下,不需要實例化一個新 TaskFactory<TResult> 實例。 可以使用靜態Task<TResult>.Factory 屬性,它返回一個工廠對象,將使用默認值。 然后可以調用其方法來啟動新任務或定義任務延續。
代碼

class Program { static void Main(string[] args) { TestTaskFactory(); } public static void TestTaskFactory() { TaskFactory<DateTime> factory = new TaskFactory<DateTime>(); Task<DateTime>[] tasks = new Task<DateTime>[] { factory.StartNew(() => { return DateTime.Now.ToUniversalTime(); }), factory.StartNew(() => { Thread.Sleep(5000); return DateTime.Now.ToUniversalTime(); }), factory.StartNew(() => { return DateTime.Now.ToUniversalTime(); }) }; StringBuilder sb = new StringBuilder(); foreach (Task<DateTime> task in tasks) sb.AppendFormat("{0}\t", task.Result); Console.WriteLine(sb.ToString()); } }
注意:任務可以分配在不同的線程池本地隊列中,由不同的線程調用並且任務間並不是串行執行,而是並行執行。
Parallel
並行,讓多個線程池線程並行工作。由於是並行執行,所以有一點需要注意:工作項彼此之間必須可以並行執行!

class Program { static void Main(string[] args) { TestParallel(); } public static void TestParallel() { Parallel.For(0, 10, i => { Console.WriteLine(i); }); List<int> lists = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; Parallel.ForEach(lists, i => { Console.WriteLine(i); }); } }
說明:
1.Parallel.For效率高於Parallel.Foreach,所以當For與Foreach都可以時,推薦使用For。
2.上面的代碼,運行For時,你可能會發現數字是有順序的打印出來,給人一種串行執行的錯覺,你可以斷點調試你的代碼,會發現確實有多個線程在運行代碼。
3.Parallel.For()、Parallel.Foreach()還有一些重載方法,大家可以結合實際情況使用,這里就不復述了。
感謝大家的耐心閱讀。