在https://www.cnblogs.com/loverwangshan/p/10415937.html中我們有講到委托的非同步方法,Thread,ThreadPool,然後今天來講一下Task, ThreadPool相比Thread來說具備了很多優勢,但是ThreadPool卻又存在一些使用上的 ...
在https://www.cnblogs.com/loverwangshan/p/10415937.html中我們有講到委托的非同步方法,Thread,ThreadPool,然後今天來講一下Task,
ThreadPool相比Thread來說具備了很多優勢,但是ThreadPool卻又存在一些使用上的不方便。比如:
- ThreadPool不支持線程的取消、完成、失敗通知等交互性操作
- ThreadPool不支持線程執行的先後次序
以往,如果開發者要實現上述功能,需要完成很多額外的工作,現在.netFramwork3.0出現的Task,線程是基於線程池,然後提供了豐富的API
下麵我們來初步認識一下Task,下麵我們先新增一個公共的方法以下方法的演示中都會用到:
1 #region Private Method 2 /// <summary> 3 /// 一個比較耗時耗資源的私有方法 4 /// </summary> 5 /// <param name="name"></param> 6 private void DoSomethingLong(string name) 7 { 8 Console.WriteLine($"****DoSomethingLong Start {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}*****"); 9 long lResult = 0; 10 for (int i = 0; i < 1000000000; i++) 11 { 12 lResult += i; 13 } 14 Console.WriteLine($"****DoSomethingLong End {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}****"); 15 } 16 #endregionView Code
一:Task啟動任務的幾種方式
1 { 2 Task task = new Task(() => this.DoSomethingLong("btnTask_Click_1")); 3 task.Start(); 4 } 5 { 6 Task task = Task.Run(() => this.DoSomethingLong("btnTask_Click_2")); 7 } 8 { 9 TaskFactory taskFactory = Task.Factory;// Task.Factory等同於: new TaskFactory() 10 Task task = taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click_3")); 11 }
二:Task.Delay()和Thread.Sleep()方法的比較運用
1 private void Test() 2 { 3 Console.WriteLine($"****************btnTask_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 4 { 5 Stopwatch stopwatch = new Stopwatch(); 6 stopwatch.Start(); 7 Console.WriteLine("在Sleep之前"); 8 Thread.Sleep(2000); //同步等待--當前線程等待2s 然後繼續 9 Console.WriteLine("在Sleep之後"); 10 stopwatch.Stop(); 11 Console.WriteLine($"Sleep耗時{stopwatch.ElapsedMilliseconds}"); 12 } 13 { 14 Stopwatch stopwatch = new Stopwatch(); 15 stopwatch.Start(); 16 Console.WriteLine("在Delay之前"); 17 Task task = Task.Delay(2000) 18 .ContinueWith(t => 19 { 20 stopwatch.Stop(); 21 Console.WriteLine($"Delay耗時{stopwatch.ElapsedMilliseconds}"); 22 Console.WriteLine($"This is ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); 23 });//非同步等待--等待2s後啟動新任務 24 Console.WriteLine("在Delay之後"); 25 } 26 Console.WriteLine($"****************btnTask_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 27 }View Code
通過執行結果如下:
通過觀察發現:
- Sleep是同步執行方法,即是遇到Sleep先等待,然後才能接著做其它的
- Delay是非同步執行方法,一般不會單獨使用,而是會跟ContinueWith等一起聯合使用,即是重新開啟一個線程,這個線程多少時間之後執行!
三:TaskFactory類的ContinueWhenAny 和 ContinueWhenAll
1 2 /// <summary> 3 /// 模擬Coding過程 4 /// </summary> 5 /// <param name="name"></param> 6 /// <param name="projectName"></param> 7 private void Coding(string name, string projectName) 8 { 9 Console.WriteLine($"***Coding Start {name} {projectName} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***"); 10 long lResult = 0; 11 for (int i = 0; i < 1000000000; i++) 12 { 13 lResult += i; 14 } 15 16 Console.WriteLine($"***Coding End {name} {projectName} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***"); 17 } 18 private void Test() 19 { 20 Console.WriteLine($"***btnTask_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***"); 21 TaskFactory taskFactory = new TaskFactory(); 22 List<Task> taskList = new List<Task>(); 23 taskList.Add(taskFactory.StartNew(o => this.Coding("AA", "Portal"), "AA")); 24 taskList.Add(taskFactory.StartNew(o => this.Coding("BB", " DBA "), "BB")); 25 taskList.Add(taskFactory.StartNew(o => this.Coding("CC", "Client"), "CC")); 26 taskList.Add(taskFactory.StartNew(o => this.Coding("DD", "BackService"), " DD")); 27 taskList.Add(taskFactory.StartNew(o => this.Coding("EE", "Wechat"), "EE")); 28 29 //誰第一個完成,獲取一個紅包獎勵 30 taskFactory.ContinueWhenAny(taskList.ToArray(), t => Console.WriteLine($"{t.AsyncState}開發完成,獲取個紅包獎勵{Thread.CurrentThread.ManagedThreadId.ToString("00")}")); 31 taskList.Add(taskFactory.ContinueWhenAll(taskList.ToArray(), rArray => Console.WriteLine($"開發都完成,一起慶祝一下{Thread.CurrentThread.ManagedThreadId.ToString("00")}"))); 32 //ContinueWhenAny ContinueWhenAll 非阻塞式的回調;而且使用的線程可能是新線程,也可能是剛完成任務的線程,唯一不可能是主線程 33 Console.WriteLine($"***btnTask_Click end {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***"); 34 }View Code
通過結果我們總結如下:
- ContinueWhenAny:等待任意一條完成
- ContinueWhenAll :等待所有的完成
- ContinueWhenAny 和 ContinueWhenAll 非阻塞式的回調;而且使用的線程可能是新線程,也可能是剛完成任務的線程,唯一不可能是主線程
四:Task中的 WaitAny 和 WaitAll
1 /// <summary> 2 /// 模擬Coding過程 3 /// </summary> 4 /// <param name="name"></param> 5 /// <param name="projectName"></param> 6 private void Coding(string name, string projectName) 7 { 8 Console.WriteLine($"***Coding Start {name} {projectName} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***"); 9 long lResult = 0; 10 for (int i = 0; i < 1000000000; i++) 11 { 12 lResult += i; 13 } 14 Thread.Sleep(2000); 15 Console.WriteLine($"***Coding End {name} {projectName} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***"); 16 } 17 private void Test() 18 { 19 Console.WriteLine($"***btnTask_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***"); 20 TaskFactory taskFactory = new TaskFactory(); 21 List<Task> taskList = new List<Task>(); 22 taskList.Add(taskFactory.StartNew(o => this.Coding("AA", "Portal"), "AA")); 23 taskList.Add(taskFactory.StartNew(o => this.Coding("BB", " DBA "), "BB")); 24 taskList.Add(taskFactory.StartNew(o => this.Coding("CC", "Client"), "CC")); 25 taskList.Add(taskFactory.StartNew(o => this.Coding("DD", "BackService"), " DD")); 26 taskList.Add(taskFactory.StartNew(o => this.Coding("EE", "Wechat"), "EE")); 27 28 //阻塞當前線程,等著任意一個任務完成 29 Task.WaitAny(taskList.ToArray());//也可以限時等待,如果是winform界面會卡 30 Console.WriteLine("第一個模塊已經完成,現在開始準備環境開始部署"); 31 //需要能夠等待全部線程完成任務再繼續 阻塞當前線程,等著全部任務完成,如果是winform界面會卡 32 Task.WaitAll(taskList.ToArray()); 33 Console.WriteLine("5個模塊全部完成,準備聯測"); 34 Console.WriteLine($"***btnTask_Click end {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***"); 35 }View Code
運行執行如下:
通過上面得到如下:
- Task.WaitAny:等待任意一條完成。例如:核心數據可能來自資料庫/介面服務/分散式搜索引擎/緩存,多線程併發請求,哪個先完成就用哪個結果,其他的就不管了
- Task.WaitAll :等待所有的完成。例如:A資料庫 B介面 C分散式服務 D搜索引擎,適合多線程併發,都完成後才能返回給用戶,需要等待WaitAll
- Task.WaitAny 和Task.WaitAll都是阻塞當前線程,等任務完成後執行操作, 阻塞卡界面,是為了併發以及順序控制
五:Task想要控制先後順序,可以通過ContinueWith
這個在什麼的Delay中已經看到了,可以一直使用ContinueWith來增加任務
1 Task.Run(() => this.DoSomethingLong("btnTask_Click")).ContinueWith(t => Console.WriteLine($"btnTask_Click已完成{Thread.CurrentThread.ManagedThreadId.ToString("00")}"));//回調
六:任務想要有返回值,並且把返回值傳遞到ContinueWith中,可以通過如下代碼實現:
1 Task.Run<int>(() => 2 { 3 Thread.Sleep(2000); 4 return DateTime.Now.Year; 5 }).ContinueWith(tInt => 6 { 7 int i = tInt.Result; //有堵塞 8 });
註意:使用.Result這個會堵塞線程
七:Task的線程是源於線程池,線程池是單例的,全局唯一
我們可以通過下麵代碼來測試一下:
1 ThreadPool.SetMaxThreads(8, 8); 2 for (int i = 0; i < 100; i++) 3 { 4 int k = i; 5 Task.Run(() => 6 { 7 Console.WriteLine($"This is {k} running ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); 8 Thread.Sleep(2000); 9 }); 10 }
設置線程池最大線程為8個後,我們通過監測結果發現:同時併發的Task只有8個,而且線程Id是重覆出現的,即線程是復用的。所以線程池是是全局的,以後要慎重設置線程池數量。
八:Parallel是來源於命名空間為:System.Threading.Tasks,併發執行多個Action 多線程。主線程會參與計算---阻塞界面,等於TaskWaitAll+主線程計算
具體的一些用法如下:
1 { 2 //啟動5個任務 3 Parallel.Invoke(() => this.DoSomethingLong("btnParallel_Click_1"), 4 () => this.DoSomethingLong("btnParallel_Click_2"), 5 () => this.DoSomethingLong("btnParallel_Click_3"), 6 () => this.DoSomethingLong("btnParallel_Click_4"), 7 () => this.DoSomethingLong("btnParallel_Click_5")); 8 } 9 { 10 //啟動5個任務,i是從0-4 11 Parallel.For(0, 5, i => this.DoSomethingLong($"btnParallel_Click_{i}")); 12 } 13 { 14 //啟動5個任務 15 Parallel.ForEach(new int[] { 0, 1, 2, 3, 4 }, i => this.DoSomethingLong($"btnParallel_Click_{i}")); 16 }
上面三種方法都是可以的。然後Parallel是線程阻塞,那我們可不可以不堵塞線程呢,這個我們可以重啟一個任務,讓子線程去做這個事情,如下:
1 Task.Run(() => 2 { 3 ParallelOptions options = new ParallelOptions(); 4 options.MaxDegreeOfParallelism = 3; 5 Parallel.For(0, 10, options, i => this.DoSomethingLong($"btnParallel_Click_{i}")); 6 });
這是一種思路,以後會有很多地方用到。
九:控制線程數量
上面的Task和TaskFactory以及Parallel都已經學習完了,我們曉的線程是根據電腦資源有關係的,有時候批量啟動N個線程,效率還不如單線程高,因為會有線程切換是要耗時的,那我們怎麼控制保證一次調用多個線程呢,下麵提供Task和Parallel的兩種解決方案,具體如下:
1 { 2 //Parallel設置最大線程為3 3 ParallelOptions options = new ParallelOptions(); 4 //設置最大線程為3,以後Parallel想要控制線程數量只需要設置ParallelOptions類中的MaxDegreeOfParallelism即可 5 options.MaxDegreeOfParallelism = 3; 6 Parallel.For(0, 10, options, i => this.DoSomethingLong($"btnParallel_Click_{i}")); 7 } 8 { 9 //Task設置最大線程為3 10 List<Task> taskList = new List<Task>(); 11 for (int i = 0; i < 10000; i++) 12 { 13 int k = i; 14 if (taskList.Count(t => t.Status != TaskStatus.RanToCompletion) >= 3) 15 { 16 Task.WaitAny(taskList.ToArray()); 17 taskList = taskList.Where(t => t.Status != TaskStatus.RanToCompletion).ToList(); 18 } 19 taskList.Add(Task.Run(() => 20 { 21 Console.WriteLine($"This is {k} running ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); 22 Thread.Sleep(2000); 23 })); 24 } 25 }View Code
學完上面的非同步方法,Thread和Task,有人會提出以下問題:
1:什麼時候能用多線程? 任務能併發的時候能夠使用多線程
2:多線程能幹嘛?多線程能夠提升速度/優化用戶體驗,以cpu資源來換時間