多執行緒之旅(Task 任務)
一、Task(任務)和ThreadPool(執行緒池)不同
原始碼
1、執行緒(Thread)是建立併發工具的底層類,但是在前幾篇文章中我們介紹了Thread的特點,和例項。可以很明顯發現侷限性(返回值不好獲取(必須在一個作用域中)),當我們執行緒執行完之後不能很好的進行下一次任務的執行,需要多次銷燬和建立,所以不是很容易使用在多併發的情況下。
2、執行緒池(ThreadPool) QueueUserWorkItem是很容易發起併發任務,也解決了上面我們的需要多次建立、銷燬的效能損耗解決了,但是我們就是太簡單的,我不知道執行緒什麼時候結束,也沒有獲取返回值的途徑,也是比較尷尬的事情。
3、任務(Task)表示一個通過或不通過執行緒實現的併發操作,任務是可組合的,使用延續(continuation)可將它們串聯在一起,它們可以使用執行緒池減少啟動延遲,可使用回撥方法避免多個執行緒同時等待I/O密集操作。
二、初識Task(任務)
1、Task(任務)是在.NET 4.0引入的、Task是在我們執行緒池ThreadPool上面進行進一步的優化,所以Task預設還是執行緒池執行緒,並且是後臺執行緒,當我們的主執行緒結束時其他執行緒也會結束
2、Task建立任務,也和之前差不多
/// <summary> /// Task 的使用 /// Task 的建立還是差不多的 /// </summary> public static void Show() { //例項方式 Task task = new Task(() => { Console.WriteLine("無返回引數的委託"); }); //無參有返回值 Task<string> task1 = new Task<string>(() => { return "我是返回值"; }); //有參有返回值 Task<string> task2 = new Task<string>(x => { return "返回值 -- " + x.ToString(); }, "我是輸入引數"); //開啟執行緒 task2.Start(); //獲取返回值 Result會堵塞執行緒獲取返回值 Console.WriteLine(task2.Result); //使用執行緒工廠建立 無引數無返回值執行緒 Task.Factory.StartNew(() => { Console.WriteLine("這個是執行緒工廠建立"); }).Start(); //使用執行緒工廠建立 有引數有返回值執行緒 Task.Factory.StartNew(x => { return "返回值 -- " + x.ToString(); ; }, "我是引數"); //直接靜態方法執行 Task.Run(() => { Console.WriteLine("無返回引數的委託"); }); }
說明:
1、事實上Task.Factory型別本身就是TaskFactory(任務工廠),而Task.Run(在.NET4.5引入,4.0版本呼叫的是後者)是Task.Factory.StartNew的簡寫法,是後者的過載版本,更靈活簡單些。
2、呼叫靜態Run方法會自動建立Task物件並立即呼叫Start
3、Task.Run等方式啟動任務並沒有呼叫Start,因為它建立的是“熱”任務,相反“冷”任務的建立是通過Task建構函式。
三、Task(任務進階)
1、Wait 等待Task執行緒完成才會執行後續動作
//建立一個執行緒使用Wait堵塞執行緒 Task.Run(() => { Console.WriteLine("Wait 等待Task執行緒完成才會執行後續動作"); }).Wait();View Code
2、WaitAll 等待Task[] 執行緒陣列全部執行成功之後才會執行後續動作
//建立一個裝載執行緒的容器 List<Task> list = new List<Task>(); for (int i = 0; i < 10; i++) { list.Add(Task.Run(() => { Console.WriteLine("WaitAll 執行"); })); } Task.WaitAll(list.ToArray()); Console.WriteLine("Wait執行完畢");View Code
3、WaitAny 等待Task[] 執行緒陣列任一執行成功之後就會執行後續動作
//建立一個裝載執行緒的容器 List<Task> list = new List<Task>(); for (int i = 0; i < 10; i++) { list.Add(Task.Run(() => { Console.WriteLine("WaitAny 執行"); })); } Task.WaitAny(list.ToArray()); Console.WriteLine("WaitAny 執行完畢");View Code
4、WhenAll 等待Task[] 執行緒陣列全部執行成功之後才會執行後續動作、與WaitAll不同的是他有回撥函式ContinueWith
//建立一個裝載執行緒的容器 List<Task> list = new List<Task>(); for (int i = 0; i < 10; i++) { list.Add(Task.Run(() => { Console.WriteLine("WhenAll 執行"); })); } Task.WhenAll(list.ToArray()).ContinueWith(x => { return x.AsyncState; }); Console.WriteLine("WhenAll 執行完畢");View Code
5、WhenAny 等待Task[] 執行緒陣列任一執行成功之後就會執行後續動作、與WaitAny不同的是他有回撥函式ContinueWith
//建立一個裝載執行緒的容器 List<Task> list = new List<Task>(); for (int i = 0; i < 10; i++) { list.Add(Task.Run(() => { Console.WriteLine("WhenAny 執行"); })); } Task.WhenAny(list.ToArray()).ContinueWith(x => { return x.AsyncState; }); Console.WriteLine("WhenAny 執行完畢"); Console.ReadLine();View Code
四、Parallel 併發控制
1、是在Task的基礎上做了封裝 4.5,使用起來比較簡單,如果我們執行100個任務,只能用到10個執行緒我們就可以使用Parallel併發控制
public static void Show5() { //第一種方法是 Parallel.Invoke(() => { Console.WriteLine("我是執行緒一號"); }, () => { Console.WriteLine("我是執行緒二號"); }, () => { Console.WriteLine("我是執行緒三號"); }); //for 方式建立多執行緒 Parallel.For(0, 5, x => { Console.WriteLine("這個看名字就知道是for了哈哈 i=" + x); }); //ForEach 方式建立多執行緒 Parallel.ForEach(new string[] { "0", "1", "2", "3", "4" }, x => Console.WriteLine("這個看名字就知道是ForEach了哈哈 i=" + x)); //這個我們包一層,就不會卡主介面了 Task.Run(() => { //建立執行緒選項 ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = 3 }; //建立一個併發執行緒 Parallel.For(0, 5, parallelOptions, x => { Console.WriteLine("限制執行的次數"); }); }).Wait(); Console.WriteLine("**************************************"); //Break Stop 都不推薦用 ParallelOptions parallelOptions = new ParallelOptions(); parallelOptions.MaxDegreeOfParallelism = 3; Parallel.For(0, 40, parallelOptions, (i, state) => { if (i == 20) { Console.WriteLine("執行緒Break,Parallel結束"); state.Break();//結束Parallel //return;//必須帶上 } if (i == 2) { Console.WriteLine("執行緒Stop,當前任務結束"); state.Stop();//當前這次結束 //return;//必須帶上 } Console.WriteLine("我是執行緒i=" + i); }); }View Code
五、多執行緒例項
1、程式碼異常我資訊大家都不陌生,比如我剛剛寫程式碼經常會報 =>物件未定義null 的真的是讓我心痛了一地,那我們的多執行緒中怎麼去處理程式碼異常呢? 和我們經常寫的同步方法不一樣,同步方法遇到錯誤會直接丟擲,當是如果我們的多執行緒中出現程式碼異常,那麼這個異常會自動傳遞呼叫Wait 或者 Task<TResult> 的Result屬性上面。任務的異常會將自動捕獲並且拋給呼叫者,為了確保報告所有的異常,CLR會將異常封裝到AggregateExcepiton容器中,這容器是公開了InnerExceptions屬性中包含所有捕獲的異常,但是如果我們的執行緒沒有等待結束不會獲取到異常。
class Program { static void Main(string[] args) { try { Task.Run(() => { throw new Exception("錯誤"); }).Wait(); } catch (AggregateException axe) { foreach (var item in axe.InnerExceptions) { Console.WriteLine(item.Message); } } Console.ReadKey(); } }View Code
/// <summary> /// 多執行緒捕獲異常 /// 多執行緒會將我們的異常吞了,因為我們的執行緒執行會直接執行完程式碼,不會去等待你捕獲到我的異常。 /// 我們的執行緒中最好是不要出現異常,自己處理好。 /// </summary> public static void Show() { //建立一個多執行緒工廠 TaskFactory taskFactory = new TaskFactory(); //建立一個多執行緒容器 List<Task> tasks = new List<Task>(); //建立委託 Action action = () => { try { string str = "sad"; int num = int.Parse(str); } catch (AggregateException ax) { Console.WriteLine("我是AggregateException 我抓到了異常啦 ax:" + ax); } catch (Exception) { Console.WriteLine("我是執行緒我已經報錯了"); } }; //這個是我們經常需要做的捕獲異常 try { //建立10個多執行緒 for (int i = 0; i < 10; i++) { tasks.Add(taskFactory.StartNew(action)); } Task.WaitAll(tasks.ToArray()); } catch (Exception ex) { Console.WriteLine("異常啦"); } Console.WriteLine("我已經執行完了"); }View Code
2、多執行緒取消機制,我們的Task在外部無法進行暫停 Thread().Abort() 無法很好控制,上上篇中Thread我們也講到了Thread().Abort() 的不足之處。有問題就有解決方案。如果我們使用一個全域性的變數控制,就需要不斷的監控我們的變數取消執行緒。那麼說當然有對應的方法啦。CancellationTokenSource (取消標記源)我們可以建立一個取消標記源,我們在建立執行緒的時候傳入我們取消標記源Token。Cancel()方法 取消執行緒,IsCancellationRequested 返回一個bool值,判斷是不是取消了執行緒了。
/// <summary> /// 多執行緒取消機制 我們的Task在外部無法進行暫停 Thread().Abort() 無法很好控制,我們的執行緒。 /// 如果我們使用一個全域性的變數控制,就需要不斷的監控我們的變數取消執行緒。 /// 我們可以建立一個取消標記源,我們在建立執行緒的時候傳入我們取消標記源Token /// Cancel() 取消執行緒,IsCancellationRequested 返回一個bool值,判斷是不是取消了執行緒了 /// </summary> public static void Show1() { //建立一個取消標記源 CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); //建立一個多執行緒工廠 TaskFactory taskFactory = new TaskFactory(); //建立一個多執行緒容器 List<Task> tasks = new List<Task>(); //建立委託 Action<object> action = x => { try { //每個執行緒我等待2秒鐘,不然 Thread.Sleep(2000); //判斷是不是取消執行緒了 if (cancellationTokenSource.IsCancellationRequested) { Console.WriteLine("放棄執行後面執行緒"); return; } if (Convert.ToUInt32(x) == 20) { throw new Exception(string.Format("{0} 執行失敗", x)); } Console.WriteLine("我是正常的我在執行"); } catch (AggregateException ax) { Console.WriteLine("我是AggregateException 我抓到了異常啦 ax:" + ax); } catch (Exception ex) { //異常出現取消後面執行的所有執行緒 cancellationTokenSource.Cancel(); Console.WriteLine("我是執行緒我已經報錯了"); } }; //這個是我們經常需要做的捕獲異常 try { //建立10個多執行緒 for (int i = 0; i < 50; i++) { int k = i; tasks.Add(taskFactory.StartNew(action, k, cancellationTokenSource.Token)); } Task.WaitAll(tasks.ToArray()); } catch (Exception ex) { Console.WriteLine("異常啦"); } Console.WriteLine("我已經執行完了"); }View Code
3、多執行緒建立臨時變數,當我們啟動執行緒之後他們執行沒有先後快慢之分,正常的迴圈中的變數也沒有作用。這個時候就要建立一個臨時變數儲存資訊,解決不訪問一個數據源。
/// <summary> /// 執行緒臨時變數 /// </summary> public static void Show2() { //建立一個執行緒工廠 TaskFactory taskFactory = new TaskFactory(); CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); //建立一個委託 Action<object> action = x => { Console.WriteLine("傳入引數 x:" + x); }; for (int i = 0; i < 20; i++) { //這最主要的就是會建立20個k的臨時變數 int k = i; taskFactory.StartNew(action, k); } Console.ReadLine(); }View Code
4、多執行緒鎖,之前我們有提到過我們的多執行緒可以同時公共資源,如果我們有個變數需要加一,但是和這個時候我們有10個執行緒同時操作這個會怎麼樣呢?
public static List<int> list = new List<int>(); public static int count = 0; public static void Show3() { //建立執行緒容器 List<Task> tasks = new List<Task>(); for (int i = 0; i < 10000; i++) { //新增執行緒 tasks.Add(Task.Run(() => { list.Add(i); count++; })); } Task.WaitAll(tasks.ToArray()); Console.WriteLine("list 行數:" + list.Count + " count 總數:" + count); Console.ReadLine(); }
我們上面的程式碼本來是count++到10000,但是我們看到結果的時候,我們是不是傻了呀,怎麼是不是說好的10000呢,其實的資料讓狗吃了?真的是小朋友有很多問號??????
5、那麼我們要怎麼去解決這個問題呢?方法還是有的今天我們要將到一個語法糖lock、它能做什麼呢?它相當於一個程式碼塊鎖,它主要鎖的是一個物件,當它鎖住物件的時候會當其他執行緒發生堵塞,因為當它鎖住程式碼時候也是鎖住了物件的訪問鏈,是其他的執行緒不能訪問。必須等待物件訪問鏈被釋放之後才能被一個執行緒訪問。我們的使用lock鎖程式碼塊的時候,儘量減少鎖入程式碼塊範圍,因為我們鎖程式碼之後會導致只有一個執行緒可以拿到資料,儘量只要必須使用lock的地方使用。
6、Lock使用要注意的地方
1、lock只能鎖引用型別的物件.
2、不能鎖空物件null某一物件可以指向Null,但Null是不需要被釋放的。(請參考:認識全面的null)。
3、lock 儘量不要去鎖string 型別雖然它是引用型別,但是string是享元模式,字串型別被CLR“暫留”
這意味著整個程式中任何給定字串都只有一個例項,就是這同一個物件表示了所有執行的應用程式域的所有執行緒中的該文字。因此,只要在應用程式程序中的任何位置處具有相同內容的字串上放置了鎖,就將鎖定應用程式中該字串的所有例項。因此,最好鎖定不會被暫留的私有或受保護成員。
4、lock就避免鎖定public 型別或不受程式控制的物件。例如,如果該例項可以被公開訪問,則 lock(this) 可能會有問題,因為不受控制的程式碼也可能會鎖定該物件。這可能導致死鎖,即兩個或更多個執行緒等待釋放同一物件。出於同樣的原因,鎖定公共資料型別(相比於物件)也可能導致問題。
/// <summary> /// 建立一個靜態物件,主要是用於鎖程式碼塊,如果是靜態的就會全域性鎖,如果要鎖例項類,就不使用靜態就好了 /// </summary> private readonly static object obj = new object(); public static List<int> list = new List<int>(); public static int count = 0; /// <summary> /// lock 多執行緒鎖 /// 當我們的執行緒訪問同一個全域性變數、同時訪問同一個區域性變數、同一個資料夾,就會出現執行緒不安全 /// 我們的使用lock鎖程式碼塊的時候,儘量減少鎖入程式碼塊範圍,因為我們鎖程式碼之後會導致只有一個執行緒可以 /// 訪問到我們程式碼塊了 /// </summary> public static void Show3() { //建立執行緒容器 List<Task> tasks = new List<Task>(); //鎖程式碼 for (int i = 0; i < 10000; i++) { //新增執行緒 tasks.Add(Task.Run(() => { //鎖程式碼 lock (obj) { //這個裡面就只會出現一個執行緒訪問,資源。 list.Add(i); count++; } //lock 是一個語法糖,就是下面的程式碼 Monitor.Enter(obj); Monitor.Exit(obj); })); } Task.WaitAll(tasks.ToArray()); Console.WriteLine("list 行數:" + list.Count + " count 總數:" + count); Console.ReadLine(); }
7、總結例項篇,雙色球例項。
1、雙色球:投注號碼由6個紅色球號碼和1個藍色球號碼組成。紅色球號碼從01--33中選擇(不重複)藍色球號碼從01--16中選擇(可以跟紅球重複),程式碼我已經實現了大家可以下載原始碼。只有自己多多倒騰才能讓自己的技術成長。 下一次我們async和await這兩個關鍵字下篇記錄