C#多線程和異步(三)——一些異步編程模式


一、任務並行庫

  任務並行庫(Task Parallel Library)是BCL中的一個類庫,極大地簡化了並行編程,Parallel常用的方法有For/ForEach/Invoke三個靜態方法。在C#中for/foreach循環使用十分普遍,如果迭代不依賴與上次迭代的結果時,把迭代放在 不同的處理器上並行處理 將很大地提高運行效率,Parallel.For和Parallel.ForEach就是為這個目的而設計的。

  看一個Parallel.For/ForEach的栗子:

          static void Main(string[] args)
         {
             //Parallel.For  計算0到6的平方
             Parallel.For(1, 6, i =>
             {
                 Console.WriteLine($"{i}的平方是{i*i}");
             });
 
             //Parallel.ForEach 計算每個字符串的長度
            string[] strs = { "We", "hold", "these", "truths" };
            Parallel.ForEach(strs, i => Console.WriteLine($"{i}有{i.Length}個字節"));
            Console.ReadKey();
        }

  運行結果:

  如果我們想並行執行多個任務,可以使用 Parallel.Invoke(Action[] actions) 方法,看一個栗子:

        static void Main(string[] args)
        {
            Parallel.Invoke(
                () => { Console.WriteLine($"並行執行任務1,線程Id為{Thread.CurrentThread.ManagedThreadId}"); },
                () => { Console.WriteLine($"並行執行任務2,線程Id為{Thread.CurrentThread.ManagedThreadId}"); }
                );
            Console.ReadKey();
        }

執行結果如下:

 

二、計時器(Timer)

  計時器提供了一種 定期重復運行異步方法 的方式,當計時器到期后,系統從線程池中的線程上開啟一個回調方法,把state作為參數,並開始運行。

Timer最常用的構造函數如下:

Timer(TimeCallback callback,object state,uint dueTime, uint period)

callback是一個返回值為void的委托,state為傳入callback的參數,dueTime為第一次調用前的時間,period為兩次調用的時間間隔

 一個栗子:

 1  class Program
 2     {
 3         int count = 0;
 4         void Run(object state)
 5         {
 6             Console.WriteLine("{0},已經調用了{1}次了", state, ++count);
 7         }
 8         static void Main(string[] args)
 9         {
10             Program p = new Program();
11             //2000毫秒后開始調用,每次間隔1000毫秒
12             Timer timer = new Timer(p.Run, "hello", 2000, 1000);
13             Console.WriteLine("Timer start");
14             
15             Console.ReadLine();
16         }
17     }

執行結果:

三、委托執行異步

  委托執行異步是早期執行異步的一種方式,特別是早幾年進行網絡編程時用的比較多。現在我們完全可以使用更優秀的其他異步編程模式去替代它。有時候我們會查看早期的代碼,我們在這里簡單介紹下委托執行異步的方法。使用委托執行異步,使用的是引用方法,如果一個委托對象在調用列表中只有一個方法(這個方法就是引用方法),它就可以異步執行這個方法。委托類有兩個方法 BeginIvoke和EndInvoke 。

   BeginInvoke :執行BeginInvoke方法時,會線程池中獲取一個獨立線程來執行引用方法,並立即返回一個實現IAsyncResult接口的對象的(該對象包含了線程池中線程運行異步方法的狀態),調用線程不阻塞,而引用方法在線程池的線程中並行執行。

   EndInvoke  : 獲取異步方法調用返回的值,並釋放資源,該方法把異步方法的返回值作為自己的返回值。

委托執行異步編程的3種模式:

  等待一直到完成(wait-until-done):在發起了異步方法,原始線程執行到EndInvoke時就中斷並且等異步方法完成完成后再繼續。

  輪詢(polling):原始線程定期檢查發起的線程是否完成(通過IAsyncResult.IsCompleted屬性判斷),如果沒有則繼續進行原始線程中的任務。

  回調(callback):原始線程一直執行,無需等待或檢查發起的線程是否完成,在發起的線程中的引用方法完成之后,發起線程會調用回調方法,由回調方法在調用EndInvoke之前處理異步方法的結果。

3.1 等待一直到完成模式

  原始線程執行到EndInvoke,如果異步任務沒有完成就一直等待

 1     delegate int MyDel(int first,int second);//委托聲明
 2     class Program
 3     {
 4         static int Sum(int x, int y)
 5         {
 6             Thread.Sleep(1000);
 7             return x + y;
 8         }
 9         static void Main(string[] args)
10         {
11             MyDel del = Sum;
12             //調用異步操作(第三個參數是回調函數,第四個參數是額外的值)
13             IAsyncResult iar = del.BeginInvoke(3, 5, null, null);
14             
15             //doSomehing...
16             
17             //執行EndInvoke,如果引用方法Sum沒有執行完成,主線程就等待其完成
18             int result = del.EndInvoke(iar);
19             Console.WriteLine(result);
20         }
21     }

3.2 輪詢模式

  定期查詢任務是否完成:

 1     delegate int MyDel(int first,int second);//委托聲明
 2     class Program
 3     {
 4         static int Sum(int x, int y)
 5         {
 6             Thread.Sleep(1000);
 7             return x + y;
 8         }
 9         static void Main(string[] args)
10         {
11             MyDel del = Sum;
12             IAsyncResult iar = del.BeginInvoke(3, 5, null, null);
13             
14             //通過iar.IsCompleted定期查詢完成狀態
15             while (!iar.IsCompleted)//IsCompleted表示調用的異步操作是否完成
16             {
17                 //doSomething
18                 Thread.Sleep(300);
19                 Console.WriteLine("no done");
20             }
21             int result = del.EndInvoke(iar);
22             Console.WriteLine(result);
23             Console.ReadKey();
24         }
25     }

3.3 回調模式

原始線程執行委托的BeginInvoke后就不管新線程的事了,委托中的引用方法執行完成后,在回調函數中獲取結果並處理,執行委托的EndInvoke方法

 1     delegate int MyDel(int first,int second);//委托聲明
 2     class Program
 3     {
 4         static int Sum(int x, int y)
 5         {
 6             Thread.Sleep(1000);
 7             return x + y;
 8         }
 9         
10         //回調方法的簽名和返回值類型必須和AsyncCallBack委托類型一致
11         //輸入參數為IAsyncResult,返回值是Void類型
12        static void CallWhenDone(IAsyncResult iar){
13            AsyncResult ar = (AsyncResult)iar;
14            MyDel del = (MyDel)ar.AsyncDelegate;
15            int result = del.EndInvoke(iar);
16            Console.WriteLine("回調函數執行EndInvoke");
17            Console.WriteLine("result:{0}", result);
18            Console.WriteLine("回調函數完成");
19         }     
20         
21         static void Main(string[] args)
22         {
23             MyDel del = Sum;
24             //執行BeginInvoke方法后原始線程就不用管了,在自定義的回調函數(CallWhenDone)中執行EndInvoke方法
25             IAsyncResult iar = del.BeginInvoke(3, 5, CallWhenDone, null);
26             Console.WriteLine("開啟新線程,異步任務完成后執行回調函數");
27             //doSomething
28             Console.WriteLine("回調執行不阻塞原始線程");
29             Console.ReadKey();
30         }
31     }

執行結果:

還有一些其他的異步編程模式如BackgroundWorker等,這里不再過多介紹。

 一點補充(Windbg)

1 cpu占用過高

  我們使用多線程時有時會遇到cpu占用過高、內存爆滿的情況,快速定位異常線程是多線程開發中必須熟悉的技能。cpu占用過高一般是由死循環造成的,看下邊一個簡單的栗子,Run方法內部有死循環,程序運行后會 占用大量的cpu資源:

namespace MyApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Run();
            Run2();
            Console.ReadKey();
        }
  //死循環,會造成cpu內存占用過高
        static void Run()
        {
            Thread th = new Thread(() =>
            {
                while (true)
                {
                    Console.WriteLine("hello windbg");
                }
            });
            th.Start();
        }
  //不會占用太高的cpu資源
        static void Run2()
        {
            Thread th = new Thread(() =>
            {
                while (true)
                {
                    Thread.Sleep(1000);
                    Console.WriteLine("hello windbg2");
                }
            });
            th.Start();
        }
    }
}

  程序運行后cpu資源占用過高,怎么去定位呢?這里采用Windbg簡單演示cpu占用過高的異常定位,下載地址:Windbg下載。安裝完成后,界面如下所示:

1.生成Dump文件

  這里MyApp生成為x64位的Release版本,點擊MyApp.exe文件運行,打開【任務管理器】,找到MyApp,右鍵選擇【創建轉儲文件】即可生成dump文件。

2.Windbg分析dump文件

  打開Windbg,選擇【文件】->【Open dump file】->找到上一步生成的dump文件即可。

   執行以下命令加載符號和sos庫

.sympath SRV*c:\localsymbols*http://msdl.microsoft.com/download/symbols
.reload
.load C:\Windows\Microsoft.NET\Framework64\v4.0.30319\SOS.DLL

  通過命令 !threads 查看線程:

   死循環會長期占有cpu,通過 !runaway 查看各個線程的運行時間:

   我們看到 4eac線程的運行時間最長,通過命令 ~~[4eac] ; !clrstack 查看線程堆棧信息:

  我們看到異常定位在MyApp的Program類的第24行,查看我們的代碼,找到這個位置,發現這里是一個while(true)死循環,定位結束。

2 內存爆滿

  內存爆滿也是異常遇到的問題,如大量拼接字符串會占用較大的內存,看下邊的一個栗子,程序代碼如下:

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("開始執行..");
            GetBigString();
            Console.ReadKey();
        }
        //大字符串拼接
        static void GetBigString()
        {
            String str = "";
            for (int i = 0; i < 10000000; i++)
            {
                str+=$"hello{i}";
            }
            Console.WriteLine(str);
        }
    }

     內存爆滿最常見原因是大量創建某個類型的變量,問題定位方法和上邊定位cpu占用高的定位差不多。首先生成dump文件,然后用Windbg打開,加載符號和sos庫,然后執行 !dumpheap –stat 查看各個類型的數量和尺寸,我們看到string類型數量和占用的資源很多:

 

  通過 !DumpHeap /d -mt 00007ff8878c74c0 查看當前的方法表,如下:

  點開一個地址,具體內容如下:

  通過字符串內容是【hello0hello1...】和string類型數量多、尺寸大,我們再去在代碼中查找很容易定位到問題代碼。

小結:Windbg可以查看clr級別內容,在開發中對我們優化代碼和異常定位有不錯的幫助,這里只是簡單介紹Windbg的基本用法,有興趣的小伙伴可以研究下官方教程

  

 


免責聲明!

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



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