System.Threading.Tasks


前言:

我們之前介紹了兩種構建多線程軟件的編程技術(使用異步委托或通過System.Threading的成員)。這兩個可以在任何版本的.NET平台工作。

關於System.Threading 的介紹

關於 System.Threading.Tasks的介紹

  從.NET4.0開始,微軟引入了一種全新的多線程應用程序開發方法,即使用TPL並行編程庫。使用System.Threading.Tasks中的類型,可以構建可擴展的並行代碼,而不必直接與線程和線程池打交道。我們使用TPL的時候也可以使用System,Thrading。這兩種線程工具可以非常自然地一起工作。

  總體而言,System.Threading.Tasks中的類型被稱為任務並行庫(Task Parallel  Library,TPL)。

此命名空間中的一些類。

 

 

Parallel類的作用

TPL中一個十分重要的類時 System.Threading.Tasks.Parallel ,它提供了大量方法,能夠以並行的方式迭代數據集合(實現了IEnumerable<T>的對象)。在SDK文檔中查看Parallel類,你會發現該類支持兩個主要的靜態方法——Parallel.For()和Parallel.FoeEach(),每個方法都有很大的重載版本。

並行解釋:我們知道foreach里面循環一次,然后再循環,再循環,那並行說的是盡可能把這些分開的一次次循環放在一起執行。加快了速度。

方法中的參數有兩個可能需要了解一下,等具體在用到的時候在解釋。

出了for,foreach方法外:

 

    這些方法可以用來編寫並行執行的碼體。這些語句的邏輯與普通循環(使用for或foreach關鍵字)中的邏輯完全相同。好處是,Parallel類將從線程池中為我們提取線程(和管理並發)。這個方法里面還需要使用System.Func<T>System.Action<T>委托(到時候會專門介紹委托的時候在仔細介紹)來指定要調用的處理數據方法。

Func<T>委托:表示一個擁有給定返回值和不同數量參數的方法。

Action<t>委托:表示指向有幾個參數的方法,但返回 void.

我們在使用For(),ForEach()方法時可以傳遞強類型的Func<T>或Action<T>委托對象,你也可以使用恰當的C#匿名方法或Lambda表達式來簡化編程。

 

使用Parallel類的數據並行

  使用TPL的第一種方式是執行數據並行。使用For,ForEach方法以並行方式對數組或集合中的數據進行迭代。

 

列子:我們把一個文件夾中的圖片,進行翻轉,然后保存在別的文件夾中

普通的寫法:

        private void ProcessFiles() 
        {
            string[] files = Directory.GetFiles(@"C:\Users\Seali\Pictures\小牛電動", "*.*", SearchOption.AllDirectories);
            string newDir = @"C:\ModefiedPictures";
            Directory.CreateDirectory(newDir);

            foreach (string item in files)
            {
                  string filename = Path.GetFileName(item);
                  using (Bitmap bitma = new Bitmap(item))
                  {
                      bitma.RotateFlip(RotateFlipType.Rotate180FlipNone);//反轉180度 
                      bitma.Save(Path.Combine(newDir, filename)); //保存

                      this.Invoke((Action)delegate
                      {
                          textBox1.Text = string.Format("Processing {0} on thread {1}", filename, Thread.CurrentThread.ManagedThreadId);
                      });

                  }
            }
 private void button1_Click(object sender, EventArgs e)
        {
            System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew(); //用來計算運行時間
            ProcessFiles();
          double aa=sw.ElapsedMilliseconds / 1000.0;
          MessageBox.Show(aa+"s"); //計算我們完成這個方法用了多長時間
}

當我們執行此方法的時候,開始就一直卡這,結束了我們只能看到文本框上顯示最后一次的名字,因為我們的線程阻塞了。因為我們在等待這個過程,所有卡頓。

 

換成我們的Parallel類的方法試一下

  string[] files = Directory.GetFiles(@"C:\Users\Seali\Pictures\小牛電動", "*.*", SearchOption.AllDirectories);
            string newDir = @"C:\ModefiedPictures";
            Directory.CreateDirectory(newDir);
                Parallel.ForEach(files, item =>       //對於集合處理很容易
                { 
                    string filename = Path.GetFileName(item);
                    using (Bitmap bitma = new Bitmap(item))
                    {
                        bitma.RotateFlip(RotateFlipType.Rotate180FlipNone);
                        bitma.Save(Path.Combine(newDir, filename));

                        this.Invoke((Action)delegate     //在form對象上調用,允許次線程以線程安全的方式訪問控件
                        {
                            textBox1.Text = string.Format("Processing {0} on thread {1}", filename, Thread.CurrentThread.ManagedThreadId);
                        });
                    }
                });

雖然速度上快了一些,但依然阻塞了。有沒有辦法讓我們的線程不再阻塞,當然是有的,介紹我們的第二個類。

 

Task

 定義:表示一個異步操作。可以作為異步委托的簡單替代品。

 

部分構造函數:

部分屬性:

本文由機器翻譯。若要查看英語原文,請勾選“英語”復選框。 也可將鼠標指針移到文本上,在彈出窗口中顯示英語原文。
翻譯
英語

TaskFactory 類

提供對創建和計划 Task 對象的支持。

部分方法:(此方法有很多的重載)

 

傳入的委托,指向以異步方式進行調用的方法。居然是異步執行了,我們的主線程不再卡頓了,每循環一次文本框都會變一次了。

跟新我們的代碼:

 //創建一個新的任務來處理文件
          Task.Factory.StartNew(() =>
          {
              System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew(); //用來計算運行時間
              ProcessFiles();
              double aa = sw.ElapsedMilliseconds / 1000.0;
              MessageBox.Show(aa + "s");
          });

我們也可以取消請求。

Parallel.For()和Pallel.ForEach()方法都支持取消標記,我們調用方法時,傳入一個ParallelOption對象,它包含一個CancekkationTokenSource對象。

 

 

ParallelOptions 類

存儲配置的方法的操作的選項 Parallel 類。

 屬性:

CancellationToken 結構

傳播有關應取消操作的通知。

 

 除此之外我們還需要了解

CancellationTokenSource 類

向應該被取消的 CancellationToken 發送信號。

 

 

 所有,我們新加一個取消按鈕。完整代碼如下 

  private CancellationTokenSource cancelToken = new CancellationTokenSource();
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
           //創建一個新的任務來處理文件
          Task.Factory.StartNew(() =>
          {
              System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew(); //用來計算運行時間
              ProcessFiles();
              double aa = sw.ElapsedMilliseconds / 1000.0;
              MessageBox.Show(aa + "s");
          });          
        }

        private void ProcessFiles() 
        {
            //設置參數
            ParallelOptions parOpts = new ParallelOptions();
            parOpts.CancellationToken = cancelToken.Token;
            parOpts.MaxDegreeOfParallelism = System.Environment.ProcessorCount;
            string[] files = Directory.GetFiles(@"C:\Users\Seali\Pictures\小牛電動", "*.*", SearchOption.AllDirectories);
            string newDir = @"C:\ModefiedPictures";
            Directory.CreateDirectory(newDir);
            try
            {
                Parallel.ForEach(files, item =>
                {
                    parOpts.CancellationToken.ThrowIfCancellationRequested();//拋異常
                    string filename = Path.GetFileName(item);
                    using (Bitmap bitma = new Bitmap(item))
                    {
                        bitma.RotateFlip(RotateFlipType.Rotate180FlipNone);
                        bitma.Save(Path.Combine(newDir, filename));

                        this.Invoke((Action)delegate     //在form對象上調用,允許次線程以線程安全的方式訪問控件
                        {
                            textBox1.Text = string.Format("Processing {0} on thread {1}", filename, Thread.CurrentThread.ManagedThreadId);
                        });

                    }
                });
            }
            catch (OperationCanceledException ex)
            {
                this.Invoke((Action)delegate //在form對象上調用,允許次線程以線程安全的方式訪問控件
                {
                    textBox1.Text = ex.Message;
                });
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            //停止所有的工作者線程
            cancelToken.Cancel();
        }

 

以上介紹的是數據並行處理。

 

使用注意:如果進行數據並行處理,循環的時候是字符串的追加,很小的機會會報錯,這個需要注意下

例如:  我把循環換成了這個,發現有時候頁面上顯示有問題,上一次還沒寫完,下一次就已經開始了,然后拼接字符串就出現了問題,如果前一次跟下一次做的操作沒什么聯系用這個就很好

 

  //Parallel.ForEach(dt.AsEnumerable().AsParallel(),
                //item =>
                //{
                //    sb.Append("<section class=\"list\"   ><span class=\"wenbe hundred\">");
                //    sb.AppendFormat("<p><u>編號:</u><em>{0}</em></p>", item["ContractNumber"]);
                //    sb.AppendFormat("<p><u>合同名稱:</u><em>{0}</em></p>", item["ContractName"]);
                //    sb.AppendFormat("<p><u>簽約單位/個人:</u><em>{0}</em></p>", item["ContractUnit"]);
                //    sb.AppendFormat("<p><u>簽約時間:</u><em>{0}</em></p>", Convert.ToDateTime(item["ContractTime"]).ToString("yyyy年MM月dd日 HH:mm:ss"));
                //    sb.AppendFormat("<p><u>合同類型:</u><em>{0}</em></p>", item["ContractTypeName"]);
                //    sb.AppendFormat("<p><u>責任社工:</u><em>{0}</em></p>", item["UserName"]);
                //    sb.Append("</span>");
                //    sb.Append("<section class=\"button\">");
                //    if (audit)
                //    {
                //        sb.AppendFormat("<a href=\"javascript:showDiv('{0}');\">審核</a>", item["ID"]);
                //    }
                //    sb.Append("</section></section>");
                //});

 

小提示:如何對DataTable里面的行並行處理。

按照我們的理解,我們把一個參數給個可迭代的類型,第二個參數給個委托,

這樣寫視乎沒毛病,第一個參數是個集合,然而顯示錯誤,如果你基礎好的話你應該知道,可以迭代的集合需要實現IEnumerable接口或聲明GetEnumerator方法的類型,

我們dt.Rows返回的僅僅只是個集合類型,並沒實現這兩個條件中的一個。

有一個非常簡單的方法可以知道你的集合適不適用與迭代,把你的集合打點 看下有沒有 AsEnumerable()  此方法,有的話就可以。

把集合換成這個就可以了

dt.AsEnumerable()

更多轉換問題就可以參數LinQ語法

 

 

 

使用並行類的任務並行

  TPL還可以使用Parallel.Invoke()方法輕松觸發多個異步任務.

列子:

點擊下載按鈕,從別的網站下載類容,然后在進行查詢

 

  string theBook;
        private void btnDownload_Click(object sender, EventArgs e)
        {
            //從別的網站下載數據,並把獲取的數據賦值給文本框
            WebClient wc = new WebClient();  //這是System.Net里面的類 //這個完成事件自己聲明
            //wc.DownloadStringCompleted += wc_DownloadStringCompleted;
            //利用委托來完成事件  效果一樣
            wc.DownloadStringCompleted += (s, eArg) =>
            {
                theBook = eArg.Result;  //得到下載的數據
                txtBook.Text = theBook;
            };           

            wc.DownloadStringAsync(new Uri("http://www.gutenberg.org/files/98/98-8.txt"));   //下載數據 不會阻止線程
        }

        void wc_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            theBook = e.Result;
            txtBook.Text = theBook;
        }

 

然后我們找出最長的單詞和出現次數最多的10個單詞

  private void btnGetStatus_Click(object sender, EventArgs e)
        {
          
                //重電子書中獲取單詞
                string[] words = theBook.Split(new char[] { ' ', '\u000A', ',', '.', ';', '-', '?', '/' }, StringSplitOptions.RemoveEmptyEntries);

                //找到最常用的個單詞
                string[] tenMostCommon = null;  //= FindTenMostCommon(words);
                //獲取最長的單詞
                string longesWord = null;// = FindLongesWord(words);

                tenMostCommon = FindTenMostCommon(words);
                longesWord = FindLongesWord(words);
                StringBuilder sb = new StringBuilder("最常見的單詞有:\n");

                foreach (string item in tenMostCommon)
                {
                    sb.AppendLine(item);
                }

                sb.AppendFormat("最長的單詞是:{0}", longesWord);
                sb.AppendLine();
                MessageBox.Show(sb.ToString(), "Book info");
        }    

        //查找出現次數前十單詞
        private string[] FindTenMostCommon(string[] words)
        {
            var freQuencyOrder = from word in words
                                 where word.Length > 6
                                 group word by word into g
                                 orderby g.Count() descending
                                 select g.Key;
            string[] commonWords = (freQuencyOrder.Take(10)).ToArray();
            return commonWords;
        }


        //查找最長的單詞
        private string FindLongesWord(string[] words) 
        {
            return (from word in words orderby word.Length descending select word).FirstOrDefault();
        }

可以修改我買的方法,讓應用程序使用所有計算機中可用的CUP,加快速度

  //盡可能的同時執行這兩個方法
                Parallel.Invoke(() =>
                {
                    tenMostCommon = FindTenMostCommon(words);
                }, () =>
                {
                    longesWord = FindLongesWord(words);
                });

 

 

並行LINQ查詢(PLINQ)

在System.Linq中的 ParallelEnumerable類提供了一擴展方法

定義:提供一組用於查詢實現 ParallelQuery{TSource} 的對象的方法。 此命令的並行等效 Enumerable

它的擴展方法太多了,這里只寫幾個

例子:

 private void btnExecute_Click(object sender, EventArgs e)
        {
            Task.Factory.StartNew(() =>  //防止線程阻塞
            {
                ProcessInfoData();
            });
        }

        private void ProcessInfoData()
        {

            int[] soure = Enumerable.Range(1, 10000000).ToArray();//生成一個很大的數組
            int[] modThreeIsZero = null;

            modThreeIsZero = (from num in soure where num % 3 == 0 orderby num descending select num).ToArray<int>();
            MessageBox.Show(string.Format("found {0} numbers that query", modThreeIsZero.Count()));
}

使用PLINQ查詢

改動一下代碼,如果可以使用TPL並行的執行該查詢,調用AsParallel()

    modThreeIsZero = (from num in soure.AsParallel() where num % 3 == 0 orderby num descending select num).ToArray<int>();

取消PLINQ查詢,跟上面的類似,把狀態傳過來就可以了,看一下完整版

  private CancellationTokenSource cancelToken = new CancellationTokenSource();
        private void btnExecute_Click(object sender, EventArgs e)
        {
            Task.Factory.StartNew(() =>  //防止線程阻塞
            {
                ProcessInfoData();
            });
        }

        private void ProcessInfoData()
        {

            int[] soure = Enumerable.Range(1, 10000000).ToArray();//生成一個很大的數組
            int[] modThreeIsZero = null;
            try
            {
                modThreeIsZero = (from num in soure.AsParallel() where num % 3 == 0 orderby num descending select num).ToArray<int>();
                MessageBox.Show(string.Format("found {0} numbers that query", modThreeIsZero.Count()));
            }
            catch (OperationCanceledException ex)
            {

                this.Invoke((Action)delegate
                {
                    this.Text = ex.Message;
                });
            }
        }

        private void btnCancel_Click(object sender, EventArgs e)
        {
            cancelToken.Cancel();
        }

 

 

.NET 4.5 下的異步調用

   注意啦,使用這個,你的版本需要到達4.5哦。

此版本新增了兩個關鍵字,來簡化了編寫異步代碼的過程。async和await關鍵字。

 

C#async和await關鍵字初深

   C#async關鍵字用來指定某個方法、Lambda表達式或匿名方法自動以一部的方式來調用。在調用async方法時,await關鍵字自動暫停但前線程中任何其他活動,直到任務完成,離開調用線程。

 例如:

   private   void btnCallMethod_Click(object sender, EventArgs e)
        {
            txtInput.Text =  DoWorkAsync();
        }

        private  string DoWorkAsync() 
        {
                Thread.Sleep(10000);
                return "Down with work!"; 
        }

當我點擊按鈕的時候,需要等待10秒鍾,文本框才能接受到類容,線程也阻塞了。用上面的方法實現起來需要寫很多。但在.NET4.5下,我們可以這么寫

在寫之前,你要了解:

  T代表返回的類型。

        //async關鍵字修飾此方法
        private  async  void btnCallMethod_Click(object sender, EventArgs e)   
        {
            txtInput.Text = await DoWorkAsync();   //使用await接受類容   記住一點就可以了  async修飾了方法,里面一定要用await修飾Task.Run()
        }

        private  Task<string>  DoWorkAsync() 
        {
            //異步執行
            var d= Task.Run(() =>
            {
                Thread.Sleep(10000);
                return "Down with work!"; 
            });
            //先談框
            MessageBox.Show(d.GetType().ToString());
            return d; 
        }

 

此方法作為非阻塞調用。在被調用的方法名前面使用了await關鍵字。這很重要:如果async關鍵字修飾某個方法,但內部沒有一個方法await方法調用,任會構建一個阻塞。

DoWork()的實現直接返回Task<T>對象,它是Task.Run()的返回值。   Task.Run(  retun T:)   Task,T>  

 

異步方法的命名預定 

 任何返回Task的方法都用“Async”作為后綴。

 

返回void的異步方法

 

   private async Task MethodAsync() 
        {
            await Task.Run(() =>
            {
                Thread.Sleep(4000);
            });
        }

單個async方法中可以擁有多個await上下文

   private async void button2_Click(object sender, EventArgs e)
        {
            await Task.Run(() => { Thread.Sleep(2000); });
            MessageBox.Show("我在做第一件事");
            await Task.Run(() =>
            {
                Thread.Sleep(2000);
            });
            MessageBox.Show("我在做第二件事");

            await Task.Run(()=>{Thread.Sleep(200);});
            MessageBox.Show("我在做第三件事");
        }

執行了此方法,每等待兩秒才會彈一次框,不會像上面那樣先彈框。

關鍵點:

方法(包括Lambda表達式和匿名方法)可以用async關鍵字標記允許該方法以非阻塞的形式進行工作

②用async關鍵字標記的方法(包括Lambda表達式和匿名方法)在遇到await關鍵字之前將以阻塞的形式運行

③單個async方法可以擁有多個await上下文

當遇到await表達式時,調用線程將掛起,直到await的任務完成。同時,控制將返回給方法的調用者(解釋了為什么每等待2秒才彈框)

⑤await關鍵字將從視圖中隱藏返回Task對象,直接返回實際的返回值。沒有返回值的方法返回可以簡單的返回void.

⑥根據命名預定,要被異步調用的方法應該以“Async”作為后綴

 

改進我們在System.Threading中的代碼

//此方法不能用async標記
        static void Main(string[] args)
        {
           
            AddAsync();
            Console.ReadLine();
         
        }
        private static async Task AddAsync() 
        {
            Console.WriteLine("開始你的表演:");
            Console.WriteLine("ID of thread in Mian():{0}", Thread.CurrentThread.ManagedThreadId);
            await Sum(10, 20);
            Console.WriteLine("其他的線程做完了");
        }

        static async Task Sum(int a, int b) 
        {
            await Task.Run(() =>
            {
                Console.WriteLine("ID of thread in Add():{0}",Thread.CurrentThread.ManagedThreadId);
                Console.WriteLine("{0}+{1} ={2}",a,b,a+b);
            });
        
        }

速度是相當的快。 

基礎也就介紹到這了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM