###### tags: `C#` # C#-多線程API * [Source Code](https://github.com/Chihhao/C_Sharp_Advance) * [Burt Zhang](https://www.youtube.com/channel/UCnA36j_KoX-Z0QsayrLCsOg/videos) 這個頻道搜集了很多個Eleven老師的教學影片,非常精彩。 --- 請參考Elevan老師的教學影片([第1集](https://www.youtube.com/watch?v=bM7s2gmnNC0&t=3935s))([第2集](https://www.youtube.com/watch?v=gzt5zG-5HPE&t=2715s))([第3集](https://www.youtube.com/watch?v=PR-r7oWlV5Y&t=5349s))([第4集](https://www.youtube.com/watch?v=WGUb59-m_eM&t=3s))([第5集](https://www.youtube.com/watch?v=B-UUii_hNbM&t=2692s))([第6集](https://www.youtube.com/watch?v=5LcsC1ufRRo)) 以下節錄我認為的重點。 * **進程(process)**:一個程序運行時,佔用的全部計算資源的總和 * **線程(thread)**:程序執行的最小單位,任何操作都是由線程完成的。線程是依託於進程存在的,一個進程可以包含多個線程,線程也可以有自己的運算資源。 * **多線程(multi-thread)**:多個執行流同時運行 * 可以簡易理解為CPU太快了,分時間片-->需要上下文切換(加載-->計算-->保存環境) * 微觀:一個核同一時刻只能執行一個線程 * 宏觀:多線程並發,多CPU、多核是可以獨立運作的 * 選購CPU時講的「4核8線程」,是指每個核有2個邏輯處理器,可以想像成有4*2=8個內核可獨立運作;此線程非彼線程。下圖是「6核12線程」的工作管理員截圖,可以特別注意到目前有3254個線程正在跑,這個線程才是這裡討論的。![](https://i.imgur.com/E2eeMik.jpg) * **同步方法(sync)**:等待方法完成計算後,再進入下一行 (我們平常寫的程式) * **異步方法(async)**:不會等待方法完成,會直接進入下一行,非阻塞,不卡UI * 異步和多線程有什麼差別: * 多線程就是多個thread並發 * 異步是硬體式的異步,但是在C#裡面寫不了 * 統稱「**異步多線程**」,反正不用太糾結「異步」與「多線程」的差別!就是指 Thread, Pool, Task這些東西。 ### 什麼是委託(delegate)? 委託是個類型(Class),需要實例化(Instance)。 **委託的目的是讓方法變成變量(實例)**,如此便能傳遞。 (其實就是C/C++的函數指標) * ex1. 無返回+無參數 ```csharp // 這是委託的宣告 public delegate void NoReturnNoPara(); // 這是準備好,要被委託的方法 static public void DoNothing_1() { Console.WriteLine("void DoNothing_1()"); } ``` ```csharp // 委託實例化 NoReturnNoPara method1 = new NoReturnNoPara(DoNothing_1); method1.Invoke(); //呼叫委託,這三個是一樣意思的 method1(); //呼叫委託,這三個是一樣意思的 DoNothing_1(); //呼叫委託,這三個是一樣意思的 ``` * ex2. 無返回+有參數 ```csharp public delegate void NoReturnWithPara(int a, int b); static public void DoNothing_2(int a, int b) { Console.WriteLine("void DoNothing_2(int a, int b)"); } ``` ```csharp NoReturnWithPara method2 = new NoReturnWithPara(DoNothing_2); method2.Invoke(3, 5); ``` * ex3. 有返回+無參數 ```csharp public delegate int WithReturnNoPara(); static public int DoNothing_3() { Console.WriteLine("int DoNothing_3()"); return 3; } ``` ```csharp WithReturnNoPara method3 = new WithReturnNoPara(DoNothing_3); int i3 = method3.Invoke(); ``` * ex4. 有返回+有參數 ```csharp public delegate int WithReturnWithPara(int a, int b); static public int DoNothing_4(int a, int b) { Console.WriteLine("int DoNothing_4(int a, int b)"); return 4; } ``` ```csharp WithReturnWithPara method4 = new WithReturnWithPara(DoNothing_4); int i4 = method4.Invoke(1, 2); ``` * ex5. 無返回+有泛型參數 ```csharp public delegate void NoReturnWithPara<T>(T t); static public void DoNothing_5(String s) { Console.WriteLine("void DoNothing_5(String s)"); } ``` ```csharp NoReturnWithPara<string> method5 = new NoReturnWithPara<string>(DoNothing_5); method5.Invoke("123"); ``` ### Lambda表達式的演進 Lambda表達式的精神就是==匿名函數== * .net 1.0 時代,就是單純的委託 ```csharp public delegate void NoReturnWithPara(int a, int b); static public void DoNothing_2(int a, int b) { Console.WriteLine("void DoNothing_2(int a, int b)"); } ``` ```csharp NoReturnWithPara method6 = new NoReturnWithPara(DoNothing_2); method6.Invoke(2, 3); ``` * .net 2.0 時代,直接把整個方法搬進來,去掉沒有意義的方法名稱,並加上 delegate 關鍵字。 在以下這個例子中,==DoNothing_2 這個函數已經被**匿名**了==。 ```csharp NoReturnWithPara method7 = new NoReturnWithPara( delegate(int a, int b) { //匿名方法 Console.WriteLine("void DoNothing_2(int a, int b)"); } ); method7.Invoke(2, 3); ``` * .net 3.0 時代,引進lambda表達式 拿掉delegate關鍵字,並加上箭頭=> **中間是箭頭=\>,左邊()是參數列表,右邊{}是方法體** ```csharp NoReturnWithPara method8 = new NoReturnWithPara( (int a, int b) => { //lambda表達式的本質是匿名方法,也就是個方法(函數) Console.WriteLine("(int a, int b)=>{...}"); } ); ``` ```csharp //參數型別可以省略,編譯器會自動推算 NoReturnWithPara method9 = new NoReturnWithPara( (a, b) => { Console.WriteLine("(a, b)=>{...}"); } ); ``` ```csharp //如果方法體只有一行,大括號與分號也能去掉 (多行就不行了) NoReturnWithPara method10 = new NoReturnWithPara( (a, b) => Console.WriteLine("(a, b)=>...") ); ``` ```csharp //實例化委託時,可以省略 new NoReturnWithPara() NoReturnWithPara method11 = (a, b) => Console.WriteLine("(a, b)=>..."); NoReturnNoPara method12 = () => Console.WriteLine("()=>..."); ``` 到這裡已經是Lambda表達是最常見的樣子了。 ### Action / Func .net 3.0 時,微軟統一了委託的寫法,==引進Action與Func,取代了Delegate==, **避免讓大家寫出各式各樣的委託** * **Action** 是.net提供的『沒有參數沒有返回值』的委託 也就相當於我們自己寫的 ```delegate void NoReturnNoPara()``` ```csharp Action action1 = () => Console.WriteLine("hello"); action1.Invoke(); ``` * **Action\<int\>** 是『接受一個int,沒有返回值』的委託 ```csharp Action<int> action2 = (a) => Console.WriteLine("(a)=>..."); Action<int> action2_1 = a => Console.WriteLine("a=>..."); //如果只有一個參數,可以把小括號去掉 action2_1.Invoke(2); ``` * **Action\<int,string\>** 是『接受一個int一個string,沒有返回值』的委託 ```csharp Action<int, string> action3 = (a, b) => Console.WriteLine("(a,b)=>..."); action3.Invoke(2, "3"); ``` * **Func\<int\>** 是『沒有參數,返回int』的委託 ```csharp Func<int> func2 = new Func<int>(() => { return 5 + 6; }); //簡化 Func<int> func3 = () => { return 5 + 6; }; //再簡化,如果方法體只有一行,大括號可以去掉,return也要去掉 Func<int> func4 = () => 5 + 6; int i1 = func4.Invoke(); //Func的調用 ``` * **Func\<int, string\>** 是『參數int,返回string』的委託 ```csharp Func<int, string> func5 = (i) => "this is string" + i.ToString(); string s = func5(5); ``` ### 同步方法與異步方法 ```csharp private void button1_Click(object sender, EventArgs e){ Action<string> action = do_something_long; //同步方法:sync,等待方法完成計算後,再進入下一行 (我們平常寫的程式) //卡UI,同步方法是走主線程(也就是UI線程) action.Invoke("User1"); //等他跑完,會卡UI action.Invoke("User2"); //等他跑完,會卡UI action.Invoke("User3"); //等他跑完,會卡UI action.Invoke("User4"); //等他跑完,會卡UI //異步方法:async,不會等待方法完成,會直接進入下一行,非阻塞,不卡UI action.BeginInvoke("User1", null, null); //開子線程執行 action.BeginInvoke("User2", null, null); //開子線程執行 action.BeginInvoke("User3", null, null); //開子線程執行 action.BeginInvoke("User4", null, null); //開子線程執行 } ``` * 提升用戶體驗:==**不卡UI,背景作業**== * 速度快,但不是線性增長-->開4個線程一起跑,速度不會變4倍 -->越複雜差越多,資源換時間,但資源不一定夠-->線程絕不是越多越好 * 多線程有管理成本:==**程式不好寫,要有想像力!**== * 有多個任務可以同時運行,如此才有多線程的價值。 例如有一個任務特別複雜,但可以切成多個子任務「獨立進行」,則適合多線程。若不能切,則不適合多線程 * ==**子線程順序不可控!啟動無序!執行時間不確定!結束也無序!**== 線程是.net向OS申請的,OS會如何分配線程?不可控! 為什麼結束也不可控?即使是同一個線程執行同一個任務,執行時間也不可控。 因為CPU時間被切片了!OS會如何分配切片?不可控! * ==**不可以用sleep的方式控制子線程順序**==,即使現在沒問題,但壞事一定會發生的。 -->應該透過回調(callback),等待狀態,等待信號量來控制順序。 ### 異步方法的控制 * 異步方法的回調(Callback) ```csharp=1 private void button2_Click(object sender, EventArgs e) { Action<string> action = do_something_long; //定義回調方法的委託 AsyncCallback callback = ia => { Console.WriteLine(" --> " + ia.AsyncState + "任務完成了"); }; Console.WriteLine("---->異步方法開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); IAsyncResult ia1 = action.BeginInvoke("User1", callback, "<User1>"); //第1個參數是Action的參數 IAsyncResult ia2 = action.BeginInvoke("User2", callback, "<User2>"); //第2個參數是回調方法的委託 IAsyncResult ia3 = action.BeginInvoke("User3", callback, "<User3>"); //第3個參數是要傳遞到回調方法的任意object IAsyncResult ia4 = action.BeginInvoke("User4", callback, "<User4>"); //就是 上面的ia.AsyncState ``` * 異步方法的控制(1) - 等待狀態 ```csharp=+ while (!ia1.IsCompleted || !ia2.IsCompleted || !ia3.IsCompleted || !ia4.IsCompleted) { Thread.Sleep(5); } ``` * 異步方法的控制(2) - 等待信號量 ```csharp=+ ia1.AsyncWaitHandle.WaitOne(); //等待任務的完成,卡介面 ia2.AsyncWaitHandle.WaitOne(5000); //等待任務的完成,但最多等5000毫秒 ia3.AsyncWaitHandle.WaitOne(); ia4.AsyncWaitHandle.WaitOne(); ``` * 異步方法的控制(3) - 結束方法: 對Func使用才有意義,可以得到結果。 當然對Action也能使用,只是就沒有返回值,效果與前面一樣。 ```csharp=+ action.EndInvoke(ia1); //EndInvoke可以主動釋放線程 action.EndInvoke(ia2); //但其實不用考慮線程釋放的問題 action.EndInvoke(ia3); //線程結束後,.net會協助釋放 action.EndInvoke(ia4); Console.WriteLine("---->異步方法結束 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); } ``` ### .net 1.0 Thread ```csharp= private void Thread_Click(object sender, EventArgs e) { Console.WriteLine("---->1.0 按鈕開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); Action action = () => do_something_long("Jack "); ThreadStart threadStart = () => do_something_long("Jack "); Thread thread = new Thread(threadStart); //Thread thread_0 = new Thread(action); //錯誤,明明 action 與 threadStart 是一樣的,但 action 就是傳不進去 //這就是為什麼要發明Action的原因:避免創造出各式各樣的委託 //Thread在1.0就有,但Action/Func是3.0才出現的 //Thread實例化時,可以簡化成這樣 Thread thread_1 = new Thread(() => do_something_long("Jack ")); Thread thread_2 = new Thread(() => do_something_long("Bill ")); //指定後台線程 (默認是前台線程) //前台線程: 線程一定要完成任務,阻止進程退出 (比較少用) //後台線程: 線程會隨著進程結束 //這個是使用thread唯一的價值,往後的task, await/async都沒有這個功能 thread_1.IsBackground = true; //預設為false(前台線程) thread_2.IsBackground = true; //線程 Priority thread_1.Priority = ThreadPriority.Highest; thread_2.Priority = ThreadPriority.Lowest; //這個只保證同一時間若是 thread_1/thread_2 同時來申請,OS 會優先給高優先的 //但到底誰先做完,還是不可控的,所以這個屬性也沒多大意義 //啟動線程 thread_1.Start(); thread_2.Start(); //等待方法 1 - Join() thread_1.Join(5000); //最多等待5000毫秒 thread_2.Join(); //等到完成 //等待方法 2 - 等待狀態 while (thread_1.ThreadState != System.Threading.ThreadState.Stopped) { Thread.Sleep(5); } Console.WriteLine("---->1.0 按鈕結束 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); //Thread還有很多方法,後來發現有缺點,造成不預期的結果,官方已不建議使用。例如: //thread_1.Suspend(); //掛起 (不一定能馬上暫停) //thread_1.Resume(); //喚醒 (不一定能馬上喚醒) //thread_1.Abort(); //停止 (不一定能馬上停止) (並且有些事情是停不住的) //等等 } ``` ### .net 2.0 Thread Pool ```csharp= private void Thread_Pool_Click(object sender, EventArgs e) { Console.WriteLine("---->2.0 按鈕開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); //1.0 Thread // (1) thread提供了太多的API,像是給了三歲小孩一把槍,造成很多誤傷 // (2) 無限使用線程,資源不可控 //2.0 Thread Pool (線程池) (「池」意味著可以反覆利用,節省創建與銷毀過程的資源浪費) // (1) 只給出必要的API // (2) 線程數量加以限制 // (3) 重用線程,避免重複的創建與銷毀 #region 線程啟動方式 { //這個版本的操作是最簡單的,但功能也是最少的 //只要一行,就可以啟動線程 //不需要操作線程(創建/啟動/銷毀),交給.net處理 ThreadPool.QueueUserWorkItem(t => do_something_long("Eric ")); //在2.0的 ThreadPool API中,沒辦法操作 暫停/恢復/銷毀/... 等動作 //ThreadPool 啥都沒有,沒有操作線程的方法 //把槍收走,避免誤傷! //連刀都不給你,只給一把筷子,你就傷害不了別人了! } #endregion #region 線程池中線程數量的管理 { int maxWorkerThreads; //線程池中的背景工作線程數最大值 (老師說平時只用到這個) int maxCompletionPortThread; //線程池中的非同步 I/O 線程數最大值 int minWorkerThreads; //線程池中的背景工作線程數最小值 (老師說平時只用到這個) int minCompletionPortThread; //線程池中的非同步 I/O 線程數最小值 ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxCompletionPortThread); //call by ref ThreadPool.GetMinThreads(out minWorkerThreads, out minCompletionPortThread); //call by ref Console.WriteLine("maxWorkerThreads = " + maxWorkerThreads); Console.WriteLine("maxCompletionPortThread = " + maxCompletionPortThread); Console.WriteLine("minWorkerThreads = " + minWorkerThreads); Console.WriteLine("minCompletionPortThread = " + minCompletionPortThread); Console.WriteLine("可以自己設定線程池的大小"); ThreadPool.SetMaxThreads(16, 16); ThreadPool.SetMinThreads(5, 5); ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxCompletionPortThread); ThreadPool.GetMinThreads(out minWorkerThreads, out minCompletionPortThread); Console.WriteLine("maxWorkerThreads = " + maxWorkerThreads); Console.WriteLine("maxCompletionPortThread = " + maxCompletionPortThread); Console.WriteLine("minWorkerThreads = " + minWorkerThreads); Console.WriteLine("minCompletionPortThread = " + minCompletionPortThread); } #endregion #region ManualResetEvent - 一個寫多線程時,常用的工具 { ManualResetEvent manualResetEvent = new ManualResetEvent(false); //這是一個類,包含了一個bool屬性,可初始化為false //可通過Set(),把屬性變true;可通過Reset(),把屬性變false //若屬性為false,則WaitOne()過不去,卡UI //若屬性為true,WaitOne()就可以過去了 //藉由ManualResetEvent,可以做到等待ThreadPool的線程完成 ThreadPool.QueueUserWorkItem(t => { do_something_long("Bill "); Console.WriteLine("Bill 完成了 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); manualResetEvent.Set(); //將屬性設置為true }); manualResetEvent.WaitOne(); //這雖然可以用,但不是一個好主意 } #endregion #region 最好不要阻礙線程池的線程 // 由於上面已經將最大線程數設定為16, // 而下面這段代碼又希望在開啟第18個線程前都在等待, // 造成死鎖! { ManualResetEvent manualResetEvent = new ManualResetEvent(false); for (int i = 0; i < 20; i++) { int k = i; ThreadPool.QueueUserWorkItem(t => { Console.WriteLine("k=" + k); if (k < 18) { manualResetEvent.WaitOne(); } else { manualResetEvent.Set(); //將屬性設置為true } }); } if (manualResetEvent.WaitOne()) { Console.WriteLine("沒有死鎖"); } } #endregion Console.WriteLine("---->2.0 按鈕結束 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); } ``` ### .net 3.0 Task 與 .net 4.5 Task.Run() ==這是目前主流寫法== ```csharp= private void task_Click(object sender, EventArgs e) { //3.0 Task // (1) Task是基於ThreadPool封裝的 // (2) 2.0時,ThreadPool的API功能太少,3.0的Task增加了多個API,滿足各種需求 Console.WriteLine("---->3.0 按鈕開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); #region Start() - 線程啟動方式 { //Start() - 啟動方式 1,取得實例,然後啟動 Task task = new Task(() => do_something_long("Jack-1 ")); task.Start(); //Start() - 啟動方式 2,可以一行啟動,但取不到實例 new Task(() => do_something_long("Jack-2 ")).Start(); //Run() - 啟動方式 3 - 這個方法在 .net 4.5 才出現 Task.Run(() => do_something_long("Eric-1 ")); //可以一行啟動 Task task_1 = Task.Run(() => do_something_long("Eric-2 ")); //可以一行啟動,並取得實例 } #endregion #region 控制線程數量也是可以用的,不過最好不要設定,用預設的就好 // ThreadPool.SetMaxThreads(16, 16); // ThreadPool.SetMinThreads(4, 4); #endregion #region ContinueWith() - 回調(Callback) { Task task = new Task(() => do_something_long("Bill ")); task.ContinueWith(t => { Console.WriteLine("Bill 工作做完了 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "]"); }); task.Start(); // 也可以寫成一行 //Task.Run(() => do_something_long("Bill ")).ContinueWith(t => { // Console.WriteLine("Bill 工作做完了 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "]"); //}); } #endregion #region WaitAll() / WaitAny() - 「全部/某個」工作完成後,才能往下走 (卡介面) { Console.WriteLine("五個工作並發運行"); List<Task> taskList = new List<Task>(); taskList.Add(Task.Run(() => do_something_long("Alex "))); taskList.Add(Task.Run(() => do_something_long("Bill "))); taskList.Add(Task.Run(() => do_something_long("Ceil "))); taskList.Add(Task.Run(() => do_something_long("Dogy "))); taskList.Add(Task.Run(() => do_something_long("Elee "))); Task.WaitAny(taskList.ToArray()); //等待任一工作完成,卡介面 //Task.WaitAny(taskList.ToArray(), 2000); //等待任一工作完成,最多2000毫秒,卡UI Console.WriteLine("任一工作做完了 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "]"); Task.WaitAll(taskList.ToArray()); //等待全部工作完成,卡介面 //Task.WaitAll(taskList.ToArray(), 2000); //等待全部工作完成,最多2000毫秒,卡UI Console.WriteLine("任一工作做完了 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "]"); } #endregion #region WhenAll() / WhenAny() - 「全部/某個」工作完成後,做某件事 (Callback)(不卡介面) { Console.WriteLine("五個工作並發運行"); List<Task> taskList = new List<Task>(); taskList.Add(Task.Run(() => do_something_long("Alex "))); taskList.Add(Task.Run(() => do_something_long("Bill "))); taskList.Add(Task.Run(() => do_something_long("Ceil "))); taskList.Add(Task.Run(() => do_something_long("Dogy "))); taskList.Add(Task.Run(() => do_something_long("Elee "))); Task.WhenAny(taskList.ToArray()).ContinueWith(t => { //不卡介面 Console.WriteLine("任一工作做完了 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "]"); }); Task.WhenAll(taskList.ToArray()).ContinueWith(t => { //不卡介面 Console.WriteLine("五個工作都做完了 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "]"); }); } #endregion #region Thread.Sleep() / Task.Delay() - 等待與延遲的差異 { //Thread.Sleep() 會卡介面 //Task.Delay() 不會卡介面 Stopwatch stopwatch = new Stopwatch(); //碼表 //Thread.Sleep()的用法 stopwatch.Restart(); Thread.Sleep(2000); //這裡會卡介面 stopwatch.Stop(); Console.WriteLine("Sleep(2000) finish, stopwatch=" + stopwatch.ElapsedMilliseconds); //Task.Delay()的錯誤用法 stopwatch.Restart(); Task.Delay(2000); //這裡不卡介面,但是也不會等待2000毫秒,這是錯誤的用法 stopwatch.Stop(); Console.WriteLine("Delay(2000) finish, stopwatch=" + stopwatch.ElapsedMilliseconds); //Task.Delay()的正確用法 stopwatch.Restart(); Task.Delay(2000).ContinueWith(t => { //不卡介面,但等了兩秒才執行ContinueWith() Console.WriteLine("Delay(2000) finish, callback start"); stopwatch.Stop(); Console.WriteLine("callback finish, stopwatch=" + stopwatch.ElapsedMilliseconds); }); } #endregion Console.WriteLine("---->3.0 按鈕結束 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); } ``` ### .net 4.0 Task Factory ```csharp= private void task_factory_Click(object sender, EventArgs e) { Console.WriteLine("---->4.0 按鈕開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); #region StartNew() - 啟動方式 { //回顧3.0 Task.Start() 的啟動方式 --> 需要兩行才能完成取得實例與啟動 Task task = new Task(() => do_something_long("Jack ")); task.Start(); //現下4.0 Task Factory 的啟動方式 --> 只要一行即可取得實例與啟動 Task task_1_1 = Task.Factory.StartNew(() => do_something_long("Vicki")); Task task_1_2 = Task.Factory.StartNew( (obj) => do_something_long("Vicki"), "<vicki>"); //可以傳一個 object 型別的「AsyncResult」進去給委託,這是Task.Start()做不到的 //未來4.5 Task.Run() 的啟動方式 --> 只要一行即可取得實例與啟動 Task task_2 = Task.Run(() => do_something_long("Eric-1 ")); } #endregion #region ContinueWhenAny() / ContinueWhenAll() { Console.WriteLine("五個工作並發運行"); List<Task> taskList = new List<Task>(); taskList.Add(Task.Factory.StartNew((obj) => do_something_long("Alex "), "<Alex>")); taskList.Add(Task.Factory.StartNew((obj) => do_something_long("Bill "), "<Bill>")); taskList.Add(Task.Factory.StartNew((obj) => do_something_long("Ceil "), "<Ceil>")); taskList.Add(Task.Factory.StartNew((obj) => do_something_long("Dogy "), "<Dogy>")); taskList.Add(Task.Factory.StartNew((obj) => do_something_long("Elee "), "<Elee>")); Task.Factory.ContinueWhenAny(taskList.ToArray(), t => { Console.WriteLine(t.AsyncState + " 工作做完了 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "]"); }); Task.Factory.ContinueWhenAll(taskList.ToArray(), t => { Console.WriteLine("五個工作都做完了 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "]"); }); } #endregion Console.WriteLine("---->4.0 按鈕結束 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); } ``` ### .net 4.5 Parallel ```csharp= private void parallel_Click(object sender, EventArgs e) { //Parallel 稱做並行編程,是基於Task的封裝 Console.WriteLine("---->4.5 Parallel 按鈕開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); #region Parallel.Invoke() - 啟動方式 { // 這樣一行,相當於之前要宣告 5 個 Task.Run,然後還要WaitAll() // 特色:主線程會參與計算,相當於節省了一個線程,但主線程就會卡介面了 Parallel.Invoke( () => do_something_long("Alex-0"), () => do_something_long("Alex-1"), () => do_something_long("Alex-2"), () => do_something_long("Alex-3"), () => do_something_long("Alex-4") ); } #endregion #region Parallel.For() { Parallel.For(0, 5, (i) => do_something_long("Alex-" + i.ToString())); } #endregion #region Parallel.Foreach() { List<string> list = new List<string>() { "A", "B", "C", "D", "E" }; Parallel.ForEach(list, (s) => do_something_long("Alex-" + s)); } #endregion Console.WriteLine("---->4.5 Parallel 按鈕結束 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); } ``` ### .net 4.5 await/async await / async 這兩個是成對的,要用一起用,單獨使用是沒有意義的 這個就是讓你寫的方便點,沒有什麼特殊的功能 --> 用寫同步的方式來寫異步 ```csharp= private async void await_async_Click(object sender, EventArgs e) { Console.WriteLine("[主線程]---->4.5 Await / Async 按鈕開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); // 所有的方法都可以用 async 修飾 //修飾前 private void NoReturnNoAwait() //修飾後 private async void NoReturnNoAwait() //只要方法被 Async 修飾了,那麼裡面就要出現 Await,否則會有綠色底線,不過可以通過編譯 //Async方法裡面沒有Await是沒有意義的 NoReturnNoAwait(); // 基本使用方法 (1) 沒有回傳值 { NoReturn(); for (int i = 0; i < 10; i++) { Console.WriteLine("[主線程] 其他任務 - " + i + " [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "]"); Thread.Sleep(500); //主線程執行,卡UI } } // 基本使用方法 (2) 回傳 Task { Task task = Return_Task(); await task.ConfigureAwait(false); //主線程到這裡就返回了 for (int i = 0; i < 10; i++) { Console.WriteLine("[子線程] 其他任務 - " + i + " [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "]"); Thread.Sleep(500); //子線程執行,不卡UI } } // 基本使用方法 (3) 回傳 帶返回值的 Task<string>: 一個string的返回值 { Task<string> task = Return_Task_String(); await task.ConfigureAwait(false); //主線程到這裡就返回了 Console.WriteLine("[子線程] 取得結果: " + task.Result); for (int i = 0; i < 10; i++) { Console.WriteLine("[子線程] 其他任務 - " + i + " [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "]"); Thread.Sleep(500); //子線程執行,不卡UI } } // 以前遇到的痛點 { // 假設一個需求情境如下 // 需要異步執行一個任務,但這個任務中,有許多小任務,寫必須依序完成 Task.Run(() => { //這裡是子線程執行 string pizza = MakePizza_NoAwaitAsync(); Console.WriteLine(" [子線程] 完成比薩 - " + pizza); }); // 這裡是主線程執行 } // await/async 的新寫法解決痛點 { //前面要加上 async 才能用 await string pizza = await MakePizza_AwaitAsync(); //這行以下都是子線程執行 Console.WriteLine(" [子線程] 完成比薩 - " + pizza); } // 最後的結論: // await/async 是會傳染的,要嘛不用,要嘛全部都要用 // await/async 就是讓你寫的方便點,沒有什麼特殊的功能 --> 用寫同步的方式來寫異步 Console.WriteLine("[主線程]---->4.5 Await / Async 按鈕結束 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); } ``` ```csharp=+ private async void NoReturnNoAwait() { // 只用 async 修飾方法,但方法裡面沒有用到 await,是沒有意義的 Console.WriteLine("[主線程]--> NoReturnNoAwait() start <-- [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Task task = Task.Run(() => { //啟動子線程 Console.WriteLine(" [子線程] 任務開始 start [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Thread.Sleep(2000); //任務 Console.WriteLine(" [子線程] 任務結束 end [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); }); Console.WriteLine("[主線程]--> NoReturnNoAwait() end <-- [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); } private async void NoReturn() { Console.WriteLine("[主線程]--> NoReturn() start <-- [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Task task = Task.Run(() => { //啟動子線程 Console.WriteLine(" [子線程] 任務開始 start [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Thread.Sleep(3000); Console.WriteLine(" [子線程] 任務結束 end [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); }); await task.ConfigureAwait(false); //主線程到這裡就返回了,執行外面的主線程任務 //多加了 ConfigureAwait(false) 表示後續的任務不一定要用主線程執行 --> 不卡UI Console.WriteLine(); Console.WriteLine(" -------------------這裡相當於是Callback方法-------------------"); Console.WriteLine(" [子線程] 這裡是 await 以後的程式碼 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Console.WriteLine(" [子線程] --> NoReturn() end <-- [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Console.WriteLine(" -------------------這裡相當於是Callback方法-------------------"); Console.WriteLine(); } private async Task Return_Task() { Console.WriteLine("[主線程]--> Return_Task() start <-- [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Task task = Task.Run(() => { //啟動子線程 Console.WriteLine(" [子線程] 任務開始 start [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Thread.Sleep(3000); Console.WriteLine(" [子線程] 任務結束 end [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); }); await task.ConfigureAwait(false); //主線程到這裡就返回了,執行外面的主線程任務 //多加了 ConfigureAwait(false) 表示後續的任務不一定要用主線程執行 --> 不卡UI Console.WriteLine(); Console.WriteLine(" -------------------這裡相當於是Callback方法-------------------"); Console.WriteLine(" [子線程] 這裡是 await 以後的程式碼 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Console.WriteLine(" [子線程] --> Return_Task() end <-- [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Console.WriteLine(" -------------------這裡相當於是Callback方法-------------------"); Console.WriteLine(); } private async Task<string> Return_Task_String() { Console.WriteLine("[主線程]--> Return_Task_String() start <-- [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Task task = Task.Run(() => { //啟動子線程 Console.WriteLine(" [子線程] 任務開始 start [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Thread.Sleep(3000); Console.WriteLine(" [子線程] 任務結束 end [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); }); await task.ConfigureAwait(false); //主線程到這裡就返回了,執行外面的主線程任務 //多加了 ConfigureAwait(false) 表示後續的任務不一定要用主線程執行 --> 不卡UI Console.WriteLine(); Console.WriteLine(" -------------------這裡相當於是Callback方法-------------------"); Console.WriteLine(" [子線程] 這裡是 await 以後的程式碼 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Console.WriteLine(" [子線程] --> Return_Task_String() end <-- [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Console.WriteLine(" -------------------這裡相當於是Callback方法-------------------"); Console.WriteLine(); return "這是Return_Task_String()的返回值"; } ``` ```csharp=+ private string MakePizza_NoAwaitAsync() { Task<string> task = Task.Run(() => 製作麵團("黃金麵粉")) .ContinueWith(t => 麵團發酵(t.Result)) .ContinueWith(t => 做成餅皮(t.Result)) .ContinueWith(t => 做成比薩(t.Result)); return task.Result; } private async Task<string> MakePizza_AwaitAsync() { string result; result = await Task.Run(() => 製作麵團("黃金麵粉")).ConfigureAwait(false); result = await Task.Run(() => 麵團發酵(result)).ConfigureAwait(false); result = await Task.Run(() => 做成餅皮(result)).ConfigureAwait(false); result = await Task.Run(() => 做成比薩(result)).ConfigureAwait(false); return result; } private string 製作麵團(string name) { Console.WriteLine(" [子線程] 用<" + name + ">製作麵團 開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Thread.Sleep(1000); Console.WriteLine(" [子線程] 用<" + name + ">製作麵團 完成 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); return "用" + name + "做好的麵團"; } private string 麵團發酵(string name) { Console.WriteLine(" [子線程] <" + name + ">發酵 開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Thread.Sleep(1000); Console.WriteLine(" [子線程] <" + name + ">發酵 完成 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); return "發酵的" + name; } private string 做成餅皮(string name) { Console.WriteLine(" [子線程] 用<" + name + ">做成餅皮 開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Thread.Sleep(1000); Console.WriteLine(" [子線程] 用<" + name + ">做成餅皮 完成 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); return "用" + name + "做成的餅皮"; } private string 做成比薩(string name) { Console.WriteLine(" [子線程] 用<" + name + ">做成比薩 開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); Thread.Sleep(1000); Console.WriteLine(" [子線程] 用<" + name + ">做成比薩 完成 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] "); return "用" + name + "做成的比薩"; } ``` ### 多線程的其他觀念 * 多線程的異常處理:**線程裡面的異常會遺失!** 建議在Action裡面包一層try-catch,在線程裡,不要出現異常;如果有異常,在線程裡面處理好. ```csharp= private void error_handling_Click(object sender, EventArgs e) { Console.WriteLine("---->多線程的異常處理開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); // 線程裡面的異常會遺失 try { List<Task> taskList = new List<Task>(); for (int i = 0; i < 20; i++) { string name = "Jack-" + i.ToString(); Action<object> action = s => { Thread.Sleep(1000); if (s.ToString() == "Jack-11") throw new Exception("Jack-11 Fail!"); //這個異常會遺失 if (s.ToString() == "Jack-12") throw new Exception("Jack-12 Fail!"); //這個異常會遺失 Console.WriteLine(s + " Finish"); }; Task task = new Task(action, name); task.Start(); taskList.Add(task); } //Task.WaitAll(taskList.ToArray()); //除非在這裡等,外面才能抓到異常,但是會卡介面 } catch (AggregateException aex) { foreach (Exception ex in aex.InnerExceptions) { Console.WriteLine("Error: " + ex.Message); } } catch (Exception ex) { Console.WriteLine("Error: " + ex.Message); } // 建議在Action裡面包一層try-catch // 在線程裡,不要出現異常;如果有異常,在線程裡面處理好 try { List<Task> taskList = new List<Task>(); for (int i = 0; i < 20; i++) { string name = "Jack-" + i.ToString(); Action<object> action = s => { try { Thread.Sleep(1000); if (s.ToString() == "Jack-11") throw new Exception("Jack-11 Fail!"); //異常不會遺失 if (s.ToString() == "Jack-12") throw new Exception("Jack-12 Fail!"); //異常不會遺失 Console.WriteLine(s + " Finish"); } catch (Exception ex) { //這裡會抓到異常 Console.WriteLine("Error In Action: " + ex.Message); } }; Task task = new Task(action, name); task.Start(); taskList.Add(task); } //Task.WaitAll(taskList.ToArray()); //除非在這裡等,外面才能抓到異常,但是會卡介面,不好 } catch (AggregateException aex) { foreach (Exception ex in aex.InnerExceptions) { Console.WriteLine("Error: " + ex.Message); } } catch (Exception ex) { Console.WriteLine("Error: " + ex.Message); } Console.WriteLine("---->多線程的異常處理結束 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); } ``` * 線程取消:**線程是沒有辦法從外部停止的**,這麼做很不好! 應該在線程內部,自己停止自己。通常的作法是定期檢查一個flag,判斷是否結束線程。 ```csharp= private void cancel_Click(object sender, EventArgs e) { Console.WriteLine("---->線程取消按鈕開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); // 定期檢查一個flag,判斷是否結束線程 CancellationTokenSource cts = new CancellationTokenSource(); //也可以用bool,但CancellationTokenSource多了一些功能(自動拋異常/不啟動) //bool bCancelThread = false; try { List<Task> taskList = new List<Task>(); for (int i = 0; i < 30; i++) { string name = "Jack-" + i.ToString(); Action<object> action = s => { try { Thread.Sleep(1000); if (s.ToString() == "Jack-11") throw new Exception("Jack-11 Fail!"); //異常不會遺失 if (s.ToString() == "Jack-12") throw new Exception("Jack-12 Fail!"); //異常不會遺失 if (cts.IsCancellationRequested) { //if (bCancelThread) { Console.WriteLine(s + " 放棄執行!"); } else { Console.WriteLine(s + " Finish"); } } catch (Exception ex) { //這裡會抓到異常 cts.Cancel(); //bCancelThread = true; Console.WriteLine("Error In Action: " + ex.Message); } }; //建立Task時,可以加入 CancellationTokenSource Task task = new Task(action, name, cts.Token); task.Start(); taskList.Add(task); } Task.WaitAll(taskList.ToArray()); } catch (AggregateException aex) { foreach (Exception ex in aex.InnerExceptions) { Console.WriteLine("Error: " + ex.Message); } } catch (Exception ex) { Console.WriteLine("Error: " + ex.Message); } Console.WriteLine("---->線程取消按鈕結束 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); } ``` * 多線程的臨時變量 (==**這是一個坑!**==) ```csharp= private void forLoop_Click(object sender, EventArgs e) { Console.WriteLine("---->多線程的臨時變量按鈕開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); for (int i = 0; i < 5; i++) { int k = i; //這行一定要寫,非常重要 Task.Run(() => Console.WriteLine("k=" + k + ", i=" + i) ); } // 全程有5個k,分別是0,1,2,3,4 // 全程只有1個i,而在打印時,i早就變成5了 Console.WriteLine("---->多線程的臨時變量按鈕結束 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); } ``` * 線程安全,這是多線程程式裡,最麻煩的事情! 同時有多個線程寫入同一個東西,就會發生==覆蓋==! (同一個變數)(同一個集合)(同一個文件)(同一個資料庫) ```csharp= private static readonly object _lock = new object(); // 線程安全鎖 private void lock_Click(object sender, EventArgs e) { Console.WriteLine("---->線程安全開始 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); // 單線程的情況 { int iTotalCount = 0; List<int> listInt = new List<int>(); for (int i = 0; i < 10000; i++) { iTotalCount++; listInt.Add(i); } Console.WriteLine("iTotalCount=" + iTotalCount); Console.WriteLine("listInt.Count=" + listInt.Count); } // 多線程的情況 (線程不安全) { int iTotalCount = 0; List<int> listInt = new List<int>(); List<Task> taskList = new List<Task>(); for (int i = 0; i < 10000; i++) { int i_new = i; taskList.Add(Task.Run(() => { iTotalCount++; listInt.Add(i_new); })); } Task.WaitAll(taskList.ToArray()); Console.WriteLine("iTotalCount=" + iTotalCount); Console.WriteLine("listInt.Count=" + listInt.Count); // 同時有多個線程寫入同一個東西,就會發生覆蓋! // (同一個變數)(同一個集合)(同一個文件)(同一個資料庫) } // region 線程安全鎖 { int iTotalCount = 0; List<int> listInt = new List<int>(); List<Task> taskList = new List<Task>(); for (int i = 0; i < 10000; i++) { int i_new = i; taskList.Add(Task.Run(() => { lock (_lock) { //線程安全鎖,保證在大括號中,只有一個線程運行 iTotalCount++; listInt.Add(i_new); } })); } Task.WaitAll(taskList.ToArray()); Console.WriteLine("iTotalCount=" + iTotalCount); Console.WriteLine("listInt.Count=" + listInt.Count); } Console.WriteLine("---->線程安全結束 [" + Thread.CurrentThread.ManagedThreadId.ToString("00") + "] " + DateTime.Now.TimeOfDay + "<----"); } ``` * **線程安全鎖**,這個方法解決了問題,但犧牲了性能,所以鎖起來的東西要越少越好。 最好能避免線程衝突,例如利用「==數據拆分==」的方式,並發寫多個小數據,最後再合成一個大數據。(多個小數據)(多個小集合)(多個小文件)... 另外,==資料庫是沒辦法拆分的==!