多線程之旅(Task 任務)

1、Task(任務)和ThreadPool(線程池)不一樣html

      源碼git

  一、線程(Thread)是建立併發工具的底層類,可是在前幾篇文章中咱們介紹了Thread的特色,和實例。能夠很明顯發現侷限性(返回值很差獲取(必須在一個做用域中)),當咱們線程執行完以後不能很好的進行下一次任務的執行,須要屢次銷燬和建立,因此不是很容易使用在多併發的狀況下。數組

  二、線程池(ThreadPool) QueueUserWorkItem是很容易發起併發任務,也解決了上面咱們的須要屢次建立、銷燬的性能損耗解決了,可是咱們就是太簡單的,我不知道線程何時結束,也沒有獲取返回值的途徑,也是比較尷尬的事情。
安全

  三、任務(Task)表示一個經過或不經過線程實現的併發操做,任務是可組合的,使用延續(continuation)可將它們串聯在一塊兒,它們可使用線程池減小啓動延遲,可以使用回調方法避免多個線程同時等待I/O密集操做。多線程

2、初識Task(任務)併發

  一、Task(任務)是在.NET 4.0引入的、Task是在咱們線程池ThreadPool上面進行進一步的優化,因此Task默認仍是線程池線程,而且是後臺線程,當咱們的主線程結束時其餘線程也會結束async

  二、Task建立任務,也和以前差很少ide

 /// <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("無返回參數的委託");
            });
        }
View Code

說明函數

  一、事實上Task.Factory類型自己就是TaskFactory(任務工廠),而Task.Run(在.NET4.5引入,4.0版本調用的是後者)是Task.Factory.StartNew的簡寫法,是後者的重載版本,更靈活簡單些。工具

  二、調用靜態Run方法會自動建立Task對象並當即調用Start

  三、Task.Run等方式啓動任務並無調用Start,由於它建立的是「熱」任務,相反「冷」任務的建立是經過Task構造函數。

3、Task(任務進階)

  一、Wait 等待Task線程完成纔會執行後續動做

 //建立一個線程使用Wait堵塞線程
            Task.Run(() =>
            {
                Console.WriteLine("Wait 等待Task線程完成纔會執行後續動做");
            }).Wait();
View Code

  二、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

  三、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

  四、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

  五、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

4、Parallel 併發控制

  一、是在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

 5、多線程實例

  一、代碼異常我信息你們都不陌生,好比我剛剛寫代碼常常會報 =>對象未定義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

  二、多線程取消機制,咱們的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

  三、多線程建立臨時變量,當咱們啓動線程以後他們執行沒有前後快慢之分,正常的循環中的變量也沒有做用。這個時候就要建立一個臨時變量存儲信息,解決不訪問一個數據源。

 /// <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

 

   四、多線程鎖,以前咱們有提到過咱們的多線程能夠同時公共資源,若是咱們有個變量須要加一,可是和這個時候咱們有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呢,其實的數據讓狗吃了?真的是小朋友有不少問號??????

 

  五、那麼咱們要怎麼去解決這個問題呢?方法仍是有的今天咱們要將到一個語法糖lock、它能作什麼呢?它至關於一個代碼塊鎖,它主要鎖的是一個對象,當它鎖住對象的時候會當其餘線程發生堵塞,由於當它鎖住代碼時候也是鎖住了對象的訪問鏈,是其餘的線程不能訪問。必須等待對象訪問鏈被釋放以後才能被一個線程訪問。咱們的使用lock鎖代碼塊的時候,儘可能減小鎖入代碼塊範圍,由於咱們鎖代碼以後會致使只有一個線程能夠拿到數據,儘可能只要必須使用lock的地方使用。

  六、Lock使用要注意的地方

      一、lock只能鎖引用類型的對象.

    二、不能鎖空對象null某一對象能夠指向Null,但Null是不須要被釋放的。(請參考:認識全面的null)。

    三、lock 儘可能不要去鎖string 類型雖然它是引用類型,可是string是享元模式,字符串類型被CLR「暫留」
這意味着整個程序中任何給定字符串都只有一個實例,就是這同一個對象表示了全部運行的應用程序域的全部線程中的該文本。所以,只要在應用程序進程中的任何位置處具備相同內容的字符串上放置了鎖,就將鎖定應用程序中該字符串的全部實例。所以,最好鎖定不會被暫留的私有或受保護成員。

    四、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();
        }

七、總結實例篇,雙色球實例。

  一、雙色球:投注號碼由6個紅色球號碼和1個藍色球號碼組成。紅色球號碼從01--33中選擇(不重複)藍色球號碼從01--16中選擇(能夠跟紅球重複),代碼我已經實現了你們能夠下載源碼。只有本身多多倒騰才能讓本身的技術成長。 下一次咱們async和await這兩個關鍵字下篇記錄

相關文章
相關標籤/搜索