Task是.NET Framework3.0出現的,線程是基於線程池的,然後提供豐富的api,Thread方法很多很強大,但是太過強大,沒有限制。 DoSomethingLong方法如下: /// <summary> /// 一個比較耗時耗資源的私有方法 /// </summary> /// <pa ...
Task是.NET Framework3.0出現的,線程是基於線程池的,然後提供豐富的api,Thread方法很多很強大,但是太過強大,沒有限制。
DoSomethingLong方法如下:
/// <summary> /// 一個比較耗時耗資源的私有方法 /// </summary> /// <param name="name"></param> private void DoSomethingLong(string name) { Console.WriteLine($"****************DoSomethingLong Start {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); long lResult = 0; for (int i = 0; i < 1_000_000_000; i++) { lResult += i; } Thread.Sleep(2000); Console.WriteLine($"****************DoSomethingLong End {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************"); }View Code
Task的使用:
{ Task task = new Task(() => this.DoSomethingLong("btnTask_Click_1")); task.Start(); } { Task task = Task.Run(() => this.DoSomethingLong("btnTask_Click_2")); } { TaskFactory taskFactory = Task.Factory; Task task = taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click_3")); }
如果這樣去調用:
ThreadPool.SetMaxThreads(8, 8); for (int i = 0; i < 100; i++) { int k = i; Task.Run(() => { Console.WriteLine($"This is {k} running ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Thread.Sleep(2000); }); }
如果去掉設置最大線程的代碼:
for (int i = 0; i < 100; i++) { int k = i; Task.Run(() => { Console.WriteLine($"This is {k} running ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Thread.Sleep(2000); }); }
運行結果如下:
ThreadPool.SetMaxThreads(8, 8);
線程池是單例的,全局唯一的,設置後,同時併發的Task只有8個,而且是復用的,Task的線程是源於線程池的,全局的,請不要這樣設置。
假如我想控制下Task的併發數量,改怎麼做?
{ Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); Console.WriteLine("在Sleep之前"); Thread.Sleep(2000);//同步等待--當前線程等待2s 然後繼續 Console.WriteLine("在Sleep之後"); stopwatch.Stop(); Console.WriteLine($"Sleep耗時{stopwatch.ElapsedMilliseconds}"); } { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); Console.WriteLine("在Delay之前"); Task task = Task.Delay(2000) .ContinueWith(t => { stopwatch.Stop(); Console.WriteLine($"Delay耗時{stopwatch.ElapsedMilliseconds}"); Console.WriteLine($"This is ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); });//非同步等待--等待2s後啟動新任務 Console.WriteLine("在Delay之後"); stopwatch.Stop(); Console.WriteLine($"Delay耗時{stopwatch.ElapsedMilliseconds}"); }
運行結果如下:
如果將最後一個stopwatch註釋掉:
{ Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); Console.WriteLine("在Sleep之前"); Thread.Sleep(2000);//同步等待--當前線程等待2s 然後繼續 Console.WriteLine("在Sleep之後"); stopwatch.Stop(); Console.WriteLine($"Sleep耗時{stopwatch.ElapsedMilliseconds}"); } { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); Console.WriteLine("在Delay之前"); Task task = Task.Delay(2000) .ContinueWith(t => { stopwatch.Stop(); Console.WriteLine($"Delay耗時{stopwatch.ElapsedMilliseconds}"); Console.WriteLine($"This is ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); });//非同步等待--等待2s後啟動新任務 Console.WriteLine("在Delay之後"); //stopwatch.Stop(); //Console.WriteLine($"Delay耗時{stopwatch.ElapsedMilliseconds}"); }
什麼時候用多線程?
任務併發是時候
多線程能幹嘛?
提升速度,優化用戶體驗。
比如,現在有一個場景,在公司開會,領導在分配任務,不能併發,因為只能有一個領導在講話分配任務,當任務分配下去,開發們確實可以同時開始擼代碼,這個是可以併發的。
TaskFactory taskFactory = new TaskFactory(); List<Task> taskList = new List<Task>(); taskList.Add(taskFactory.StartNew(() => this.Coding("bingle1", "Portal"))); taskList.Add(taskFactory.StartNew(() => this.Coding("bingle2", " DBA "))); taskList.Add(taskFactory.StartNew(() => this.Coding("bingle3", "Client"))); taskList.Add(taskFactory.StartNew(() => this.Coding("bingle4", "BackService"))); taskList.Add(taskFactory.StartNew(() => this.Coding("bingle5", "Wechat")));
現在要求,誰第一個完成,獲得紅包獎勵(ContinueWhenAny);所有完成後,一起慶祝下(ContinueWhenAll),將其放入一個List<Task>裡面去
TaskFactory taskFactory = new TaskFactory(); List<Task> taskList = new List<Task>(); taskList.Add(taskFactory.StartNew(() => this.Coding("bingle1", "Portal"))); taskList.Add(taskFactory.StartNew(() => this.Coding("bingle2", " DBA "))); taskList.Add(taskFactory.StartNew(() => this.Coding("bingle3", "Client"))); taskList.Add(taskFactory.StartNew(() => this.Coding("bingle4", "BackService"))); taskList.Add(taskFactory.StartNew(() => this.Coding("bingle5", "Wechat"))); //誰第一個完成,獲取一個紅包獎勵 taskFactory.ContinueWhenAny(taskList.ToArray(), t => Console.WriteLine($"XXX開發完成,獲取個紅包獎勵{Thread.CurrentThread.ManagedThreadId.ToString("00")}")); //項目完成後,一起慶祝一下 taskList.Add(taskFactory.ContinueWhenAll(taskList.ToArray(), rArray => Console.WriteLine($"開發都完成,一起慶祝一下{Thread.CurrentThread.ManagedThreadId.ToString("00")}")));
ContinueWhenAny ContinueWhenAll 非阻塞式的回調;而且使用的線程可能是新線程,也可能是剛完成任務的線程,唯一不可能是主線程
//阻塞當前線程,等著任意一個任務完成 Task.WaitAny(taskList.ToArray());//也可以限時等待 Console.WriteLine("準備環境開始部署"); //需要能夠等待全部線程完成任務再繼續 阻塞當前線程,等著全部任務完成 Task.WaitAll(taskList.ToArray()); Console.WriteLine("5個模塊全部完成後,集中點評");
Task.WaitAny WaitAll都是阻塞當前線程,等任務完成後執行操作,阻塞卡界面,是為了併發以及順序控制,網站首頁:A資料庫 B介面 C分散式服務 D搜索引擎,適合多線程併發,都完成後才能返回給用戶,需要等待WaitAll,列表頁:核心數據可能來自資料庫/介面服務/分散式搜索引擎/緩存,多線程併發請求,哪個先完成就用哪個結果,其他的就不管了。
假如說我想控制下Task的併發數量,該怎麼做? 20個
List<Task> taskList = new List<Task>(); for (int i = 0; i < 10000; i++) { int k = i; if (taskList.Count(t => t.Status != TaskStatus.RanToCompletion) >= 20) { Task.WaitAny(taskList.ToArray()); taskList = taskList.Where(t => t.Status != TaskStatus.RanToCompletion).ToList(); } taskList.Add(Task.Run(() => { Console.WriteLine($"This is {k} running ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Thread.Sleep(2000); })); }
Parallel併發執行多個Action線程,主線程會參與計算---阻塞界面。等於TaskWaitAll+主線程計算
Parallel.Invoke(() => this.DoSomethingLong("btnParallel_Click_1"), () => this.DoSomethingLong("btnParallel_Click_2"), () => this.DoSomethingLong("btnParallel_Click_3"), () => this.DoSomethingLong("btnParallel_Click_4"), () => this.DoSomethingLong("btnParallel_Click_5"));
Parallel.For(0, 5, i => this.DoSomethingLong($"btnParallel_Click_{i}"));
Parallel.ForEach(new int[] { 0, 1, 2, 3, 4 }, i => this.DoSomethingLong($"btnParallel_Click_{i}")); ParallelOptions options = new ParallelOptions(); options.MaxDegreeOfParallelism = 3; Parallel.For(0, 10, options, i => this.DoSomethingLong($"btnParallel_Click_{i}"));
有沒有辦法不阻塞?
Task.Run(() => { ParallelOptions options = new ParallelOptions(); options.MaxDegreeOfParallelism = 3; Parallel.For(0, 10, options, i => this.DoSomethingLong($"btnParallel_Click_{i}")); });
幾乎90%以上的多線程場景,以及順序控制,以上的Task的方法就可以完成,如果你的多線程場景太複雜搞不定,那麼請梳理一下你的流程,簡化一下。建議最好不要線程嵌套線程,兩三次勉強能懂,三層就hold不住了,更多的只能求神。
多線程異常:
try { List<Task> taskList = new List<Task>(); for (int i = 0; i < 100; i++) { string name = $"btnThreadCore_Click_{i}"; taskList.Add(Task.Run(() => { if (name.Equals("btnThreadCore_Click_11")) { throw new Exception("btnThreadCore_Click_11異常"); } else if (name.Equals("btnThreadCore_Click_12")) { throw new Exception("btnThreadCore_Click_12異常"); } else if (name.Equals("btnThreadCore_Click_38")) { throw new Exception("btnThreadCore_Click_38異常"); } Console.WriteLine($"This is {name}成功 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); })); } //多線程裡面拋出的異常,會終結當前線程;但是不會影響別的線程; //那線程異常哪裡去了? 被吞了, //假如我想獲取異常信息,還需要通知別的線程 Task.WaitAll(taskList.ToArray());//1 可以捕獲到線程的異常 } catch (AggregateException aex)//2 需要try-catch-AggregateException { foreach (var exception in aex.InnerExceptions) { Console.WriteLine(exception.Message); } } catch (Exception ex)//可以多catch 先具體再全部 { Console.WriteLine(ex); } //線程異常後經常是需要通知別的線程,而不是等到WaitAll,問題就是要線程取消 //工作中常規建議:多線程的委托裡面不允許異常,包一層try-catch,然後記錄下來異常信息,完成需要的操作
線程取消:
//多線程併發任務,某個失敗後,希望通知別的線程,都停下來,how? //Thread.Abort--終止線程;向當前線程拋一個異常然後終結任務;線程屬於OS資源,可能不會立即停下來 //Task不能外部終止任務,只能自己終止自己(上帝才能打敗自己) //cts有個bool屬性IsCancellationRequested 初始化是false //調用Cancel方法後變成true(不能再變回去),可以重覆cancel try { CancellationTokenSource cts = new CancellationTokenSource(); List<Task> taskList = new List<Task>(); for (int i = 0; i < 50; i++) { string name = $"btnThreadCore_Click_{i}"; taskList.Add(Task.Run(() => { try { if (!cts.IsCancellationRequested) Console.WriteLine($"This is {name} 開始 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Thread.Sleep(new Random().Next(50, 100)); if (name.Equals("btnThreadCore_Click_11")) { throw new Exception("btnThreadCore_Click_11異常"); } else if (name.Equals("btnThreadCore_Click_12")) { throw new Exception("btnThreadCore_Click_12異常"); } else if (name.Equals("btnThreadCore_Click_13")) { cts.Cancel(); } if (!cts.IsCancellationRequested) { Console.WriteLine($"This is {name}成功結束 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); } else { Console.WriteLine($"This is {name}中途停止 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); return; } } catch (Exception ex) { Console.WriteLine(ex.Message); cts.Cancel(); } }, cts.Token)); } //1 準備cts 2 try-catch-cancel 3 Action要隨時判斷IsCancellationRequested //儘快停止,肯定有延遲,在判斷環節才會結束 Task.WaitAll(taskList.ToArray()); //如果線程還沒啟動,能不能就別啟動了? //1 啟動線程傳遞Token 2 異常抓取 //在Cancel時還沒有啟動的任務,就不啟動了;也是拋異常,cts.Token.ThrowIfCancellationRequested } catch (AggregateException aex) { foreach (var exception in aex.InnerExceptions) { Console.WriteLine(exception.Message); } } catch (Exception ex) { Console.WriteLine(ex.Message); }View Code
臨時變數:
for (int i = 0; i < 5; i++) { Task.Run(() => { Console.WriteLine($"This is btnThreadCore_Click_{i} ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }); }
為什麼運行結果後,都是5呢?
臨時變數問題,線程是非阻塞的,延遲啟動的;線程執行的時候,i已經是5了
那麼該如何解決呢?
每次都聲明一個變數k去接收,k是閉包裡面的變數,每次迴圈都有一個獨立的k,5個k變數 1個i變數
for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { Console.WriteLine($"This is btnThreadCore_Click_{i}_{k} ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }); }
這樣再運行,結果就正常了。
線程安全&lock:
線程安全:如果你的代碼在進程中有多個線程同時運行這一段,如果每次運行的結果都跟單線程運行時的結果一致,那麼就是線程安全的
線程安全問題一般都是有全局變數/共用變數/靜態變數/硬碟文件/資料庫的值,只要多線程都能訪問和修改
發生是因為多個線程相同操作,出現了覆蓋,怎麼解決?
1 Lock解決多線程衝突
Lock是語法糖,Monitor.Enter,占據一個引用,別的線程就只能等著
推薦鎖是private static readonly object,
A不能是Null,可以編譯不能運行;
B 不推薦lock(this),外面如果也要用實例,就衝突了
//Test test = new Test(); //Task.Delay(1000).ContinueWith(t => //{ // lock (test) // { // Console.WriteLine("*********Start**********"); // Thread.Sleep(5000); // Console.WriteLine("*********End**********"); // } //}); //test.DoTest(); //C 不應該是string; string在記憶體分配上是重用的,會衝突 //D Lock裡面的代碼不要太多,這裡是單線程的 Test test = new Test(); string student = "水煮魚"; Task.Delay(1000).ContinueWith(t => { lock (student) { Console.WriteLine("*********Start**********"); Thread.Sleep(5000); Console.WriteLine("*********End**********"); } }); test.DoTestString(); //2 線程安全集合 //System.Collections.Concurrent.ConcurrentQueue<int> //3 數據分拆,避免多線程操作同一個數據;又安全又高效 for (int i = 0; i < 10000; i++) { this.iNumSync++; } for (int i = 0; i < 10000; i++) { Task.Run(() => { lock (Form_Lock)//任意時刻只有一個線程能進入方法塊兒,這不就變成了單線程 { this.iNumAsync++; } }); } for (int i = 0; i < 10000; i++) { int k = i; Task.Run(() => this.iListAsync.Add(k)); } Thread.Sleep(5 * 1000); Console.WriteLine($"iNumSync={this.iNumSync} iNumAsync={this.iNumAsync} listNum={this.iListAsync.Count}"); //iNumSync 和 iNumAsync分別是多少 9981/9988 1到10000以內