以下是學習筆記:
回顧:
Thread線程和ThreadPool線程池
Thread:我們可以開啟一個線程。但是請大家記住:線程開啟會在空間和時間上有不小的開銷。所以,不能隨便開。
ThreadPool:會根據你的CPU的核心數開啟一個最合適的線程數量。如果你操作中,非常耗時,就不要用線程池,如果耗時十幾分鍾,那就不合適線程池了。
Task=>Thread + ThreadPool結合 ,使用多線程,盡量使用Task
1,Task和各種任務阻塞、延續及其線程鎖Lock
#region Task使用【1】多線程任務的開啟3種方式 //【1】通過new的方式創建一個Task對象,並啟動 static void Method1_1() { Task task1 = new Task(() => { //在這個地方編寫我們需要的邏輯... Console.WriteLine($"new一個新的Task啟動的子線程Id={Thread.CurrentThread.ManagedThreadId}"); }); task1.Start(); } //【2】使用Task的Run()方法 static void Method1_2() { Task task2 = Task.Run(() => { //在這個地方編寫我們需要的邏輯... Console.WriteLine($"使用Task的Run()方法開啟的子線程Id={Thread.CurrentThread.ManagedThreadId}"); }); } //1和2對比 //1,靈活開啟線程,想什么時候開啟就什么時候開啟 //2, 馬上開啟線程 //【3】使用TaskFactory啟動(類似於ThreadPool) static void Method1_3() { Task task3 = Task.Factory.StartNew(() => { //在這個地方編寫我們需要的邏輯... Console.WriteLine($"使用TaskFactory開啟的子線程Id={Thread.CurrentThread.ManagedThreadId}"); }); } #endregion #region Task使用【2】Task的阻塞方式和任務延續 //【1】回顧之前使用Thread多個子線程執行時阻塞的方法 static void Method2() { Thread thread1 = new Thread(() => { Thread.Sleep(2000); Console.WriteLine("Child Thread (1)......"); }); Thread thread2 = new Thread(() => { Thread.Sleep(1000); Console.WriteLine("Child Thread (2)......"); }); thread1.Start(); thread2.Start(); //... thread1.Join();//讓調用線程阻塞 thread2.Join(); //如果有很多的thread,是不是也得有很多的Join?還有,我們只希望其中一個執行完以后,后面的其他線程就能執行,這個也做不了! Console.WriteLine("This is Main Thread!"); } //【2】Task各種【阻塞】方式(3個) static void Method3() { Task task1 = new Task(() => { Thread.Sleep(1000); Console.WriteLine($"Task1子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(2000); Console.WriteLine($"Task2子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); task2.Start(); ////第1種方式:挨個等待和前面一樣 //task1.Wait(); //task2.Wait(); ////第2種方式:等待所有的任務完成 【推薦】 Task.WaitAll(task1, task2); //第3種方式:等待任何一個完成即可 【推薦】 //Task.WaitAny(task1, task2); Console.WriteLine("主線程開始運行!Time=" + DateTime.Now.ToLongTimeString()); /* 第2中方式結果: Task1子線程Id=4 21:46:58 Task2子線程Id=3 21:46:59 主線程開始運行!Time=21:46:59 第3種方式結果 Task1子線程Id = 3 21:41:34 主線程開始運行!Time = 21:41:34 Task2子線程Id = 4 21:41:35 */ } //Task任務的延續:WhenAll 希望前面所有任務執行完畢后,再繼續執行后面的線程,和前面相比,既有阻塞,又有延續。 static void Method4() { Task task1 = new Task(() => { Thread.Sleep(1000); Console.WriteLine($"Task1子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(2000); Console.WriteLine($"Task2子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); task2.Start(); //線程的延續(主線程不等待,子線程依次執行,如果你需要主線程也按照子線程的順序來,請你自己把主線程的任務放到延續任務中就可以) //線運行主線程,然后task1和task2都執行完,再執行task3 Task.WhenAll(task1, task2).ContinueWith(task3 => { //在這里可以編寫你需要的業務... Console.WriteLine($"Task3子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); Console.WriteLine("主線程開始運行!Time=" + DateTime.Now.ToLongTimeString()); /* 主線程開始運行!Time = 21:44:46 Task1子線程Id = 3 21:44:47 Task2子線程Id = 4 21:44:48 Task3子線程Id = 3 21:44:48 */ } //Task的延續:WhenAny static void Method5() { Task task1 = new Task(() => { Thread.Sleep(1000); Console.WriteLine($"Task1子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(2000); Console.WriteLine($"Task2子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); task2.Start(); //線程的延續(主線程不等待,子線程任何一個執行完畢,就會執行后面的線程) Task.WhenAny(task1, task2).ContinueWith(task3 => { //在這里可以編寫你需要的業務... Console.WriteLine($"Task3子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); Console.WriteLine("主線程開始運行!Time=" + DateTime.Now.ToLongTimeString()); /* 主線程開始運行!Time=21:48:51 Task1子線程Id=3 21:48:52 Task3子線程Id=6 21:48:52 Task2子線程Id=4 21:48:53 */ } #endregion #region Task使用【3】Task常見枚舉 TaskCreationOptions(父子任務運行、長時間運行的任務處理) //請大家通過Task的構造方法,觀察TaskCreationOptions這個枚舉的類型,自己通過F12查看 static void Method6() { Task parentTask = new Task(() => { Task task1 = new Task(() => { Thread.Sleep(1000); Console.WriteLine($"Task1子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }, TaskCreationOptions.AttachedToParent); Task task2 = new Task(() => { Thread.Sleep(3000); Console.WriteLine($"Task2子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }, TaskCreationOptions.AttachedToParent); task1.Start(); task2.Start(); }); parentTask.Start(); parentTask.Wait();//等待附加的子任務全部完成。相當於Task.WaitAll(taks1,task2); //TaskCreationOptions.AttachedToParent如果這個枚舉參數不添加,主線程會直接運行,不等待 Console.WriteLine("主線程開始執行!Time= " + DateTime.Now.ToLongTimeString()); /* Task1子線程Id=4 21:52:17 Task2子線程Id=5 21:52:19 主線程開始執行!Time= 21:52:19 */ } //長時間的任務運行,需要采取的方法 static void Method7() { Task task1 = new Task(() => { Thread.Sleep(2000); Console.WriteLine($"Task1子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }, TaskCreationOptions.LongRunning); //LongRunning:如果你明確知道這個任務是長時間運行的,建議你加上。 //當然你使用Thread也是可以的。但是不要使用ThreadPool,因為長時間占用不歸還線程,系統會強制開啟新的線程,會一定程度影響性能 task1.Start(); task1.Wait(); Console.WriteLine("主線程開始執行!Time= " + DateTime.Now.ToLongTimeString()); /* Task1子線程Id=3 21:57:42 主線程開始執行!Time= 21:57:42 */ } #endregion #region Task使用【4】Task中的取消功能:使用的是CacellationTokenSoure解決多任務中協作取消和超時取消方法 //【1】Task任務的取消和判斷 static void Method8() { //創建取消信號源對象 CancellationTokenSource cts = new CancellationTokenSource(); Task task = Task.Factory.StartNew(() => { int i = 0; while (!cts.IsCancellationRequested) //判斷任務是否被取消 { Thread.Sleep(200); i++; Console.WriteLine( $"執行次數:{i},子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); } }, cts.Token); //我們在這個地方模擬一個事件產生,如果發生某個錯誤,就取消線程 Thread.Sleep(2000); cts.Cancel(); //取消任務,只要傳遞這樣一個信號就可以 /* 執行次數:1,子線程Id=3 22:06:18 執行次數:2,子線程Id=3 22:06:18 執行次數:3,子線程Id=3 22:06:18 執行次數:4,子線程Id=3 22:06:18 執行次數:5,子線程Id=3 22:06:19 執行次數:6,子線程Id=3 22:06:19 執行次數:7,子線程Id=3 22:06:19 執行次數:8,子線程Id=3 22:06:19 執行次數:9,子線程Id=3 22:06:19 執行次數:10,子線程Id=3 22:06:20 */ } //【2】Task任務取消:同時我們也希望做一些清理的工作,也就是取消這個動作會觸發一個任務。 static void Method9() { CancellationTokenSource cts = new CancellationTokenSource(); Task task = Task.Factory.StartNew(() => { while (!cts.IsCancellationRequested) { Thread.Sleep(500); Console.WriteLine($"子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); } }, cts.Token); //注冊一個委托:這個委托將在任務取消的時候調用 cts.Token.Register(() => { //在這個地方可以編寫自己要處理的邏輯... Console.WriteLine($"任務取消,開始清理工作......{DateTime.Now.ToLongTimeString()}"); Thread.Sleep(2000); Console.WriteLine($"任務取消,清理工作結束......{DateTime.Now.ToLongTimeString()}"); }); //這個地方肯定是有其他的邏輯來控制取消 Thread.Sleep(3000);//模擬其他的耗時工作 cts.Cancel();//取消任務 /* 子線程Id=3 22:12:52 子線程Id=3 22:12:53 子線程Id=3 22:12:53 子線程Id=3 22:12:54 子線程Id=3 22:12:54 任務取消,開始清理工作......22:12:55 子線程Id=3 22:12:55 任務取消,清理工作結束......22:12:57 */ } //【3】Task任務延時自動取消:比如我們請求一個遠程接口,如果長時間沒有返回數據,我們可以做一個時間限制,超時可以取消任務(比如微信紅包退回) static void Method10() { CancellationTokenSource cts = new CancellationTokenSource(); // CancellationTokenSource cts = new CancellationTokenSource(3000); Task task = Task.Factory.StartNew(() => { while (!cts.IsCancellationRequested) { Thread.Sleep(300); Console.WriteLine($"子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); } }, cts.Token); //注冊一個委托:這個委托將在任務取消的時候調用 cts.Token.Register(() => { //在這個地方可以編寫自己要處理的邏輯... Console.WriteLine($"任務取消,開始清理工作......{DateTime.Now.ToLongTimeString()}"); Thread.Sleep(2000); Console.WriteLine($"任務取消,清理工作結束......{DateTime.Now.ToLongTimeString()}"); }); cts.CancelAfter(3000); //3秒后自動取消 /* 子線程Id=3 22:16:49 子線程Id=3 22:16:50 子線程Id=3 22:16:50 子線程Id=3 22:16:50 子線程Id=3 22:16:50 子線程Id=3 22:16:51 子線程Id=3 22:16:51 子線程Id=3 22:16:51 子線程Id=3 22:16:52 任務取消,開始清理工作......22:16:52 子線程Id=3 22:16:52 任務取消,清理工作結束......22:16:54 */ } #endregion #region Task使用【5】Task中專門的異常處理:AggregateException //AggregateException:是一個異常集合,因為Task中可能拋出異常,所以我們需要新的類型來收集異常對象 static void Method11() { var task = Task.Factory.StartNew(() => { var childTask1 = Task.Factory.StartNew(() => { //實際開發中這個地方寫你處理的業務,可能會發生異常.... //自己模擬一個異常 throw new Exception("my god!Exception from childTask1 happend!"); }, TaskCreationOptions.AttachedToParent); var childTask2 = Task.Factory.StartNew(() => { throw new Exception("my god!Exception from childTask2 happend!"); }, TaskCreationOptions.AttachedToParent); }); try { try { task.Wait(); //1.異常拋出的時機(等待task執行完畢,這里是等到異常拋出) } catch (AggregateException ex) //2.異常所在位置 { foreach (var item in ex.InnerExceptions) { Console.WriteLine(item.InnerException.Message + " " + item.GetType().Name); } //3.異常集合,如果你想往上拋,需要使用Handle方法處理一下 ex.Handle(p => { if (p.InnerException.Message == "my god!Exception from childTask1 happend!") return true;//就結束了,不往上拋了 else return false; //返回false表示往上繼續拋出異常 }); } } catch (Exception ex) { Console.WriteLine("-----------------------------------------------------"); Console.WriteLine(ex.InnerException.InnerException.Message); } /* my god!Exception from childTask2 happend! AggregateException my god!Exception from childTask1 happend! AggregateException ----------------------------------------------------- my god!Exception from childTask2 happend! */ } #endregion #region 監視鎖:Lock 限制線程個數的一把鎖 //為什么要用鎖?在多線程中,尤其是靜態資源的訪問,必然會有競爭 private static int nums = 0; private static object myLock = new object(); static void Method12() { for (int i = 0; i < 5; i++) { //開啟5線程調用一個nums Task.Factory.StartNew(() => { //TestMethod1();//不加鎖的結果順序是亂的,1,3,2,4,6,9,,,,500 TestMethod2();//加鎖的結果順序是對的,因為把資源給鎖住了,1,2,3,4,5,6,,,,500 }); } } static void TestMethod1() { for (int i = 0; i < 100; i++) { nums++; Console.WriteLine(nums); } } static void TestMethod2() { for (int i = 0; i < 100; i++) { lock (myLock) { nums++; Console.WriteLine(nums); } } } //Lock是Monitor語法糖,本質是解決資源的鎖定問題 //我們鎖住的資源一定是讓線程可訪問到的,所以不能是局部變量。 //鎖住的資源千萬不要是值類型。 //lock也不能鎖住string類型。 } #endregion
2,Task中的跨線程訪問控件和UI耗時任務卡頓的解決方法
//普通方法 private void btnUpdate_Click(object sender, EventArgs e) { Task task = new Task(() => { this.lblInfo.Text = "來自Task的數據更新:我們正在學習多線程!"; }); //task.Start(); //這樣使用會報錯 //使用下面的方式解決報錯的問題 task.Start(TaskScheduler.FromCurrentSynchronizationContext());//使用任務調度器 } //針對UI耗時的情況,單獨重載其實並不是很好 private void btnUpdate_Click1(object sender, EventArgs e) { Task task = new Task(() => { //模擬耗時(這個地方會卡主) Thread.Sleep(5000);//界面會卡5秒鍾,多線程不是萬能,多線程並不是解決卡界面的。 this.lblInfo.Text = "來自Task的數據更新:我們正在學習多線程!"; }); //task.Start(); //這樣使用會報錯 //使用下面的方式解決報錯的問題 task.Start(TaskScheduler.FromCurrentSynchronizationContext()); } //以后耗時任務都可以用這個方法 //針對耗時任務,我們可以使用新的方法 private void btnUpdate_Click2(object sender, EventArgs e) { this.btnUpdate.Enabled = false; this.lblInfo.Text = "數據更新中,請等待......"; Task task =Task.Factory.StartNew(() => { Thread.Sleep(5000); //有耗時的任務,我們可以放到ThreadPool中 }); //在ContinueWith中更新我們的數據 task.ContinueWith(t => { this.lblInfo.Text = "來自Task的數據更新:我們正在學習多線程!"; this.btnUpdate.Enabled = true; },TaskScheduler.FromCurrentSynchronizationContext()); //更新操作到同步的上下文中 }