2017年9月28日 星期四

C# : 使用多執行緒 Thread 讀取多網站資料

當你進行 C# 程式開發的時候,同一個時間,只會有一個執行緒來幫助您執行程式,例如,當我們想要讀取四個網址的內容的時候,我們會寫類似如下的程式碼;因此,當我們執行方法 SequenceThreads() 的時候,就會依序執行方法內的程式碼,所以,當第一個 GetWebContent 方法執行完成之後,才會繼續執行下一個 GetWebContent 方法,直到四個網站資料都讀取完成。

了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [HttpClient Class] 的使用方式



在進行抓取網站資料的時候,我們會呼叫方法 GetWebContent,這裡會使用 Thread.CurrentThread.ManagedThreadId 顯示出當時正在執行的受管理的執行緒ID代碼,讓你清楚現在的是使用哪個執行緒來執行;所以,再進行非多工模式的抓取網頁資料的測試程式碼的時候,所看到的受管理的執行緒ID,都是一樣的。
雖然,我們使用 HttpClient 類別的 client.GetStringAsync(url.ToString()).Result 表示式來取得特定網頁上的資料,不過,因為使用了 Result 屬性,所以,這行表示式是採用同步的方式執行的,不是非同步的方式。
    static string[] Urls = new string[]
    {
        "https://www.microsoft.com",
        "https://tw.yahoo.com/",
        "http://www.msn.com/zh-tw/",
        "https://world.taobao.com/"
    };

    private static void SequenceThreads()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        GetWebContent(Urls[0]);
        GetWebContent(Urls[1]);
        GetWebContent(Urls[2]);
        GetWebContent(Urls[3]);
        sw.Stop();
        Console.WriteLine($"抓取四個網站內容 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
        Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
        Console.ReadKey();
    }

    private static void GetWebContent(object url)
    {
        Console.WriteLine($"{url} 執行緒 ID {Thread.CurrentThread.ManagedThreadId}");

        Stopwatch sw = new Stopwatch();
        sw.Start();
        using (var client = new HttpClient())
        {
            string content = client.GetStringAsync(url.ToString()).Result;
            Console.WriteLine($"{url} 的內容大小為 {content.Length} bytes");
        }
        sw.Stop();
        Console.WriteLine($"{url} 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
    }
我們從這個測試過程的輸出內容,可以看到全部抓取四個網頁完成所需要的時間,為這四個抓取網頁的時間總和。
底下是執行結果輸出內容
https://www.microsoft.com 執行緒 ID 1
https://www.microsoft.com 的內容大小為 1020 bytes
https://www.microsoft.com 花費時間為 271.7432 ms
https://tw.yahoo.com/ 執行緒 ID 1
https://tw.yahoo.com/ 的內容大小為 194903 bytes
https://tw.yahoo.com/ 花費時間為 141.2671 ms
http://www.msn.com/zh-tw/ 執行緒 ID 1
http://www.msn.com/zh-tw/ 的內容大小為 45733 bytes
http://www.msn.com/zh-tw/ 花費時間為 161.3392 ms
https://world.taobao.com/ 執行緒 ID 1
https://world.taobao.com/ 的內容大小為 220949 bytes
https://world.taobao.com/ 花費時間為 314.1775 ms
抓取四個網站內容 花費時間為 921.3975 ms
Press any key for continuing...
現在,讓我們使用多執行緒的方式來同時抓取這些網頁的資料,在此,我們建立了四個執行緒 Thread 物件,並且在建構函是傳遞了 ParameterizedThreadStart 類別的物件;而這個 ParameterizedThreadStart型別其實是個委派型別 public delegate void ParameterizedThreadStart(object obj);,也就是說,我們要建立的執行緒物件,需要一個委派方法,如此,這個執行緒將會多工執行這個方法。
在這裡,我們在要開始進行多執行緒運作的時候,使用 Thread.CurrentThread.ManagedThreadId 屬性,顯示出這個應用程式的主執行緒 ID。
為了要能夠量測出同時抓取四個網頁的總共花費時間,我們使用了執行緒的 Join 方法,這是微軟官方的說明 封鎖呼叫執行緒,直到此執行個體所代表的執行緒終止為止
    private static void MultiThreads()
    {
        Thread thread1 = new Thread(GetWebContent);
        Thread thread2 = new Thread(GetWebContent);
        Thread thread3 = new Thread(GetWebContent);
        Thread thread4 = new Thread(GetWebContent);

        Console.WriteLine($"主執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine($"啟動執行緒");
        Stopwatch sw = new Stopwatch();
        sw.Start();
        thread1.Start(Urls[0]);
        thread2.Start(Urls[1]);
        thread3.Start(Urls[2]);
        thread4.Start(Urls[3]);

        thread1.Join();
        thread2.Join();
        thread3.Join();
        thread4.Join();

        sw.Stop();
        Console.WriteLine($"抓取四個網站內容 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
        Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
        Console.ReadKey();
    }
從執行結果可以看的出來,同時抓取四個網頁所需要花費的總共時間,大約為抓取某個網頁花費最多的時間,而不是累計總和時間。
底下是執行結果輸出內容
主執行緒 ID 1
啟動執行緒
https://world.taobao.com/ 執行緒 ID 13
https://tw.yahoo.com/ 執行緒 ID 11
http://www.msn.com/zh-tw/ 執行緒 ID 12
https://www.microsoft.com 執行緒 ID 10
https://www.microsoft.com 的內容大小為 1020 bytes
https://www.microsoft.com 花費時間為 47.7937 ms
https://world.taobao.com/ 的內容大小為 220949 bytes
https://world.taobao.com/ 花費時間為 77.1401 ms
http://www.msn.com/zh-tw/ 的內容大小為 45597 bytes
http://www.msn.com/zh-tw/ 花費時間為 126.1487 ms
https://tw.yahoo.com/ 的內容大小為 195623 bytes
https://tw.yahoo.com/ 花費時間為 946.1321 ms
抓取四個網站內容 花費時間為 973.0811 ms
Press any key for continuing...
最後,我們一樣要進行多工的抓取網頁工作,只不過,我們在這裡使用 執行緒的集區 Thread Pool 來處理這樣需求,微軟官方對於 執行緒的集區 的定義為:提供執行緒的集區,可用來執行工作、張貼工作項目、處理非同步 I/O、代表其他執行緒等候,以及處理計時器,ThreadPool 類別為您的應用程式提供了受到系統管理的背景工作執行緒集區,讓您專注於應用程式工作上,而不是執行緒的管理。 如果您有需要在背景處理的簡短工作,Managed 執行緒集區是利用多重執行緒的一個簡單方式。
ThreadPool類別的 QueueUserWorkItem 靜態方法,可以接受兩個引數,第一個為委派類型的 WaitCallback (他的委派宣告為 public delegate void WaitCallback(Object state);),另外一個是要傳入到 WaitCallback 委派函式內的方法引數。
執行緒集區執行緒為背景執行緒
    private static void MultiThreadPool()
    {
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[0]);
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[1]);
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[2]);
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[3]);
        Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
        Console.ReadKey();
    }
底下是執行結果輸出內容
https://www.microsoft.com 執行緒 ID 16
https://world.taobao.com/ 執行緒 ID 22
http://www.msn.com/zh-tw/ 執行緒 ID 18
https://tw.yahoo.com/ 執行緒 ID 15
https://www.microsoft.com 的內容大小為 1020 bytes
https://www.microsoft.com 花費時間為 151.1066 ms
http://www.msn.com/zh-tw/ 的內容大小為 46023 bytes
http://www.msn.com/zh-tw/ 花費時間為 160.9714 ms
https://world.taobao.com/ 的內容大小為 220949 bytes
https://world.taobao.com/ 花費時間為 225.9449 ms
https://tw.yahoo.com/ 的內容大小為 191718 bytes
https://tw.yahoo.com/ 花費時間為 816.9125 ms

底下是完整的測試程式碼
class Program
{
    static string[] Urls = new string[]
    {
        "https://www.microsoft.com",
        "https://tw.yahoo.com/",
        "http://www.msn.com/zh-tw/",
        "https://world.taobao.com/"
    };
    static void Main(string[] args)
    {
        SequenceThreads();

        MultiThreads();

        MultiThreadPool();
    }

    private static void MultiThreadPool()
    {
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[0]);
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[1]);
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[2]);
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[3]);
        Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
        Console.ReadKey();
    }

    private static void SequenceThreads()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        GetWebContent(Urls[0]);
        GetWebContent(Urls[1]);
        GetWebContent(Urls[2]);
        GetWebContent(Urls[3]);
        sw.Stop();
        Console.WriteLine($"抓取四個網站內容 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
        Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
        Console.ReadKey();
    }

    private static void MultiThreads()
    {
        Thread thread1 = new Thread(GetWebContent);
        Thread thread2 = new Thread(GetWebContent);
        Thread thread3 = new Thread(GetWebContent);
        Thread thread4 = new Thread(GetWebContent);

        Console.WriteLine($"主執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine($"啟動執行緒");
        Stopwatch sw = new Stopwatch();
        sw.Start();
        thread1.Start(Urls[0]);
        thread2.Start(Urls[1]);
        thread3.Start(Urls[2]);
        thread4.Start(Urls[3]);

        thread1.Join();
        thread2.Join();
        thread3.Join();
        thread4.Join();

        sw.Stop();
        Console.WriteLine($"抓取四個網站內容 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
        Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
        Console.ReadKey();
    }

    private static void GetWebContent(object url)
    {
        Console.WriteLine($"{url} 執行緒 ID {Thread.CurrentThread.ManagedThreadId}");

        Stopwatch sw = new Stopwatch();
        sw.Start();
        using (var client = new HttpClient())
        {
            string content = client.GetStringAsync(url.ToString()).Result;
            Console.WriteLine($"{url} 的內容大小為 {content.Length} bytes");
        }
        sw.Stop();
        Console.WriteLine($"{url} 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
    }
}

了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [HttpClient Class] 的使用方式






沒有留言:

張貼留言