多線程之旅(Task 任務)

来源:https://www.cnblogs.com/chenxi001/archive/2020/04/11/12676756.html
-Advertisement-
Play Games

一、Task(任務)和ThreadPool(線程池)不同 源碼 1、線程(Thread)是創建併發工具的底層類,但是在前幾篇文章中我們介紹了Thread的特點,和實例。可以很明顯發現局限性(返回值不好獲取(必須在一個作用域中)),當我們線程執行完之後不能很好的進行下一次任務的執行,需要多次銷毀和創建 ...


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

說明

  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這兩個關鍵字下篇記錄


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Flask 藍圖進行路由分發Flask雖然說是一個輕型web框架,但也總不能用一個py文件寫完全部view吧,所以我們要把路由分到不同的py文件中。這就需要用到藍圖了。 一 創建一個py文件 用於處理分過來的url,如創建music.py from flask import Blueprint mu ...
  • 新冠疫情結束在即,各位小伙伴想必也開始工作了吧...... 2020年伊始,世界仿佛開了一個大玩笑。好在天佑中華,武漢也解封了,一切都在向好的地方發展。希望小伙伴們的工作和生活沒有受到太大的影響。 據我7年以來的開發經驗,工業級別的代碼,幾乎三分之二都是在處理異常情況。而且我們去面試,面試官考察應試 ...
  • 概念:跨站腳本攻擊(XSS)是指惡意攻擊者往Web頁面里插入惡意Script代碼,當用戶瀏覽該頁之時,嵌入其中Web裡面的Script代碼會被執行,從而達到惡意攻擊用戶的目的。XSS漏洞通常是通過php的輸出函數將javascript代碼輸出到html頁面中,通過用戶本地瀏覽器執行的,所以xss漏洞 ...
  • 這裡我就直接給出答案實現Runnanle介面,並實現它的run方法繼承Thread類,並重寫它的run方法為什麼說是兩種,可能有小伙伴在網上搜索,發現可能各種說法都有,但是在Oracle的官方文檔中明確的寫了,創建線程的方式是兩種,也就是我上面說的這兩種。我們來看看這兩種方式具體的用法Runnabl... ...
  • 需求分析1.賬戶包括活期儲蓄賬戶和信用卡儲蓄賬戶。2.將賬戶信息包括存取記錄等等保存至文件中。3.異常處理包括餘額不足,輸入信息不對等等。源代碼https://github.com/zhuifeng17/DBMSsystem ...
  • 代碼和註釋 ...
  • 寫在前面 從事互聯網系統開發的人員大多希望成為資深的架構師或領域專家。但大部分人員由於自身工作環境及條件的限制,缺少大型系統實踐經驗,或者對核心的案例缺乏真實的瞭解,因此很難有機會理解分散式設計中的關鍵問題及應對方案。如何才能找到有效的方法並早日成為資深系統架構師呢? 資料獲取方法 內容簡介 本書圍 ...
  • 1、strtoupper()函數 把字元串轉換為大寫。 strtoupper(string) 2、file_get_contents file_get_contents() 把整個文件讀入一個字元串中。 該函數是用於把文件的內容讀入到一個字元串中的首選方法。 3、sql中concat函數的使用(字元 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...