2017年9月30日 星期六

C# : 網路下載檔案之下載進度事件 event 方法

在上一個 C# : 網路下載檔案之下載進度委派方法 練習中,我們使用的委派型別,來做為通知使用者最新的下載進度,在這裡,我們將會使用 C# 的事件 Event,來做出相同效果的程式碼。
由於我們需要透過事件來回報處理進度,因此,我們需要客製化一個類別,用來處理這件需求,在這裡,我們需要在新建立的類別,讓他繼承 EventArgs 類別,在這個新的類別,DownloadFileEventArgs,我們新增一個屬性,用來說明處理進度百分比。
另外,對於 Task.Factory.StartNew,我們有做些調整,在這裡,我們將修飾詞 async 移除了,所以,現在的現在傳入的委派 Lambda 將會是:Task.Factory.StartNew(() => {...};由於沒有了 async 修飾詞,所以,在 Task.Factory.StartNew 引數用的委派方法,我們就要進行調整,把原先這個委派方法內有使用到 await 的地方,全部修改成為同步的執行方法;不過,在這裡,你不用擔心,因為我們使用了 Task.Factory.StartNew 啟動一個全新的工作,這個工作會使用 ThreadPool 執行緒集區內的一個執行緒來執行這個工作,所以,也是多工處理的作業。
當要進行回報處理進度事件的時候,我們需要呼叫這個方法:onProgress(this, new DownloadFileEventArgs() { Percent = percent });,在第二個引數,我們需要透過 DownloadFileEventArgs 產生的物件,將現在處理進度傳送到訂閱者的綁定方法中。
public class DownloadFileEventArgs : EventArgs
{
    public int Percent { get; set; }
}

public class DownloadFile
{
    public event EventHandler<DownloadFileEventArgs> OnProgress;
    //public string Url { get; set; } = "http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/js_api_reference.pdf";
    public string Url { get; set; } = "https://www.tutorialspoint.com/csharp/csharp_tutorial.pdf";
    public Task Download()
    {
        var task = Task.Factory.StartNew(() =>
        {
            int offset = 5120;
            int percent = 0;
            int currentDonload = 0;
            int getBytes;
            int lastPercent = -1;
            using (var client = new HttpClient())
            {
                using (var response = client.GetAsync(Url).Result)
                {
                    var total = int.Parse(response.Content.Headers.First(h => h.Key.Equals("Content-Length")).Value.First());
                    byte[] content = new byte[total+offset];
                    using (var stream = response.Content.ReadAsStreamAsync().Result)
                    {
                        var onProgress = OnProgress;
                        while ((getBytes = stream.Read(content, currentDonload, offset)) > 0)
                        {
                            currentDonload += getBytes;
                            percent = (int)((1.0 * currentDonload / total) * 100);
                            if (lastPercent != percent)
                            {
                                if (onProgress != null)
                                {
                                    onProgress(this, new DownloadFileEventArgs() { Percent = percent });
                                }
                                lastPercent = percent;
                            }
                        }
                    }
                }
            }
        });
        return task;
    }
}
現在,讓我們開始進行測試,我們同樣需要訂閱 OnProgress 的事件,不過,您會看到輸出結果卻是不太相同,這並不是我們使用事件的方式,你可以知道發生了甚麼問題以及差異在哪裡?
static async Task Main(string[] args)
{
    DownloadFile downloadFile = new DownloadFile();
    downloadFile.OnProgress += DownloadStatus;
    Console.WriteLine($"開始進行非同步檔案檔案下載");
    await downloadFile.Download();
    Console.WriteLine($"{Environment.NewLine}Press any key to Exist...");
    Console.ReadKey();
}

private static void DownloadStatus(object sender, DownloadFileEventArgs e)
{
    if (e.Percent % 10 == 0)
    {
        Console.Write($" {e.Percent}% ");
    }
    else
    {
        Console.Write($".");
    }
}
底下是執行結果
開始進行非同步檔案檔案下載
 0% ......... 10% ......... 20% ......... 30% ......... 40% ......... 50% ......... 60% ......... 70% ......... 80% ......... 90% ......... 100%
Press any key to Exist...

C# : 網路下載檔案之下載進度委派 delegate 方法

現在,讓我們來使用委派方法的功能,在這個練習中,我們將會設計一個類別,他會下載一個網路 PDF 檔案,當在下載的過程之中,會將下載檔案的實際讀取到的檔案完成百分比,透過委派方法回報;這個委派方法扮演著一個類似事件,也就是可以接收到處理工作當時狀態的通知,所以,訂閱這個委派事件的用戶端,就可以即時下載進度顯示在螢幕上,讓使用者明瞭這個長時間的工作,現在處理到甚麼進度了。
在這個類別 DownloadFile,我們定義了一個 Download 函式,他使用了 HttpClient.GetAsync 方法,抓取指定 URL 的內容,並且使用 HTTP Header 的 Content-Length 得到要下載的檔案總大小,接著,取得 HttpResponseMessage 之後,使用 HttpResponseMessage.Content.ReadAsStreamAsync() 方法,取得 Stream 行別的物件,便可以開始每次讀取 5120 bytes 大小的內容到程式中。每次完成讀取 5120 bytes,就會呼叫委派方法 onProgress,將處理進度回報,而這個 onProgress 是個委派型別的變數 public delegate void ProgressDelegate(int percent);
    public class DownloadFile
    {
        public delegate void ProgressDelegate(int percent);
        public ProgressDelegate OnProgress;
        public string Url { get; set; } = "http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/js_api_reference.pdf";
        public async void Download()
        {
            var task = Task.Factory.StartNew(async () =>
              {
                  int offset = 5120;
                  int percent = 0;
                  int currentDonload = 0;
                  int getBytes;
                  int lastPercent = -1;
                  using (var client = new HttpClient())
                  {
                      using (var response = await client.GetAsync(Url))
                      {
                          var total = int.Parse(response.Content.Headers.First(h => h.Key.Equals("Content-Length")).Value.First());
                          byte[] content = new byte[total];
                          using (var stream = await response.Content.ReadAsStreamAsync())
                          {
                              var onProgress = OnProgress;
                              while ((getBytes = stream.Read(content, currentDonload, offset)) > 0)
                              {
                                  currentDonload += getBytes;
                                  percent = (int)((1.0*currentDonload / total) * 100);
                                  if (lastPercent != percent)
                                  {
                                      if (onProgress != null)
                                      {
                                          onProgress(percent);
                                      }
                                      lastPercent = percent;
                                  }
                              }
                          }
                      }
                  }
              });
        }
    }
在進行測試的時候,我們建立了這個類別 (DownloadFile) 物件,接者設定 OnProgress 委派欄位,使其綁定到 DownloadStatus 方法上;這個方法就會將現在的處理進度,即時顯示在螢幕上,讓使用者知道現在的處理最新情況。
static void Main(string[] args)
{
    DownloadFile downloadFile = new DownloadFile();
    downloadFile.OnProgress += DownloadStatus;
    Console.WriteLine($"開始進行非同步檔案檔案下載");
    downloadFile.Download();
    Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
    Console.ReadKey();
}

private static void DownloadStatus(int percent)
{
    if(percent%10==0)
    {
        Console.Write($" {percent}% ");
    }
    else
    {
        Console.Write($".");
    }
}
底下是執行結果
開始進行非同步檔案檔案下載
Press any key to Exist...

 0% ......... 10% ......... 20% ......... 30% ......... 40% ......... 50% ......... 60% ......... 70%

2017年9月29日 星期五

C# : 使用Async 和 Await 讀取多網站資料

最後,讓我們使用 C# 5.0 的 存取修飾詞 Async / 運算子 Await,寫出一個具有非同步的應用程式;我們這裡一樣會需要去網路抓取四個網頁內容。
首先,我們寫個類別,這個類別裡面只有個方法,GetWebContent,這個方法的回傳值為 Task 物件,而且,我們看到了,在這個方法名稱前面,有用到存取修飾詞 Async,如果您在方法或運算式上使用這個修飾詞,則它是指非同步方法。
另外,在這個非同步方法裡面,我們使用 HttpClient 類別實例化出來的物件,讀取網頁資料,我們在這裡使用了 await client.GetStringAsync(url.ToString());,這裡使用到 await 運算子,而 await 運算子會套用至非同步方法中的工作以暫停執行方法,直到等候的工作完成為止,另外,在其中使用 await 的非同步方法必須由 async 關鍵字修改。當然,在這個方法內,我們還是一樣會進行了解當時使用的執行緒 ID 上哪個以及所耗費的時間。
    public class MyClass
    {
        public async Task GetWebContent(string url)
        {
            Console.WriteLine($"{url} 執行緒 ID {Thread.CurrentThread.ManagedThreadId}");

            Stopwatch sw = new Stopwatch();
            sw.Start();
            using (var client = new HttpClient())
            {
                string content = await client.GetStringAsync(url.ToString());
                Console.WriteLine($"{url} 的內容大小為 {content.Length} bytes");
            }
            sw.Stop();
            Console.WriteLine($"{url} 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
        }
    }
要使用剛剛寫好的類別,則是相當的簡單,我們需要使用 await 運算子來呼叫 GetWebContent 方法,如同 await myClass.GetWebContent(Urls[0]);
  • 我們對於程式進入點函式 Main,使用這樣方式來宣告 async Task Main(string[] args),並且,你的開發環境需要能夠支援 C# 7.1
  • 您可以嘗試將呼叫 myClass.GetWebContent 方法前面的 await 運算子移除,看看會有何結果。
    底下是移除 await 後的執行結果
https://www.microsoft.com 執行緒 ID 1
https://tw.yahoo.com/ 執行緒 ID 1
http://www.msn.com/zh-tw/ 執行緒 ID 1
https://world.taobao.com/ 執行緒 ID 1
抓取四個網站內容 花費時間為 109.2726 ms
Press any key for continuing...

https://www.microsoft.com 的內容大小為 1020 bytes
https://www.microsoft.com 花費時間為 295.5076 ms
https://world.taobao.com/ 的內容大小為 220947 bytes
https://world.taobao.com/ 花費時間為 283.5818 ms
http://www.msn.com/zh-tw/ 的內容大小為 46320 bytes
http://www.msn.com/zh-tw/ 花費時間為 389.8703 ms
https://tw.yahoo.com/ 的內容大小為 201849 bytes
https://tw.yahoo.com/ 花費時間為 635.2415 ms
底下測試用的程式碼
        static async Task Main(string[] args)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            MyClass myClass = new MyClass();
            await myClass.GetWebContent(Urls[0]);
            await myClass.GetWebContent(Urls[1]);
            await myClass.GetWebContent(Urls[2]);
            await myClass.GetWebContent(Urls[3]);

            sw.Stop();
            Console.WriteLine($"抓取四個網站內容 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
            Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
            Console.ReadKey();
       }
底下是執行結果內容
https://www.microsoft.com 執行緒 ID 1
https://www.microsoft.com 的內容大小為 1020 bytes
https://www.microsoft.com 花費時間為 525.7561 ms
https://tw.yahoo.com/ 執行緒 ID 9
https://tw.yahoo.com/ 的內容大小為 201849 bytes
https://tw.yahoo.com/ 花費時間為 962.6452 ms
http://www.msn.com/zh-tw/ 執行緒 ID 8
http://www.msn.com/zh-tw/ 的內容大小為 46320 bytes
http://www.msn.com/zh-tw/ 花費時間為 168.9015 ms
https://world.taobao.com/ 執行緒 ID 8
https://world.taobao.com/ 的內容大小為 220947 bytes
https://world.taobao.com/ 花費時間為 267.2545 ms
抓取四個網站內容 花費時間為 1947.116 ms
Press any key for continuing...

C# : 使用多個工作 task 讀取多網站資料

在這篇文章中,我們將會延續上一篇文章,需要同時讀取四個網頁上的資料,不過,在這裡,我們將會使用 C# 提供的 Task 類別來做到這樣需求。這裡是微軟官方對於 TAP 的需求說明:工作式非同步模式 (TAP) 是以 System.Threading.Tasks 命名空間中的 System.Threading.Tasks.Task 和 System.Threading.Tasks.Task<TResult> 類型為基礎,這兩種類別用於表示任意非同步作業。 在新開發非同步專案時建議使用TAP模式。
在這裡的練習中,我們使用了方法 Task.Factory.StartNew 來建立一個工作,這個靜態方法要接受一個 Action 委派方法作為引數,這個委派方法就是要來進行非同步執行的方法。
Task.WhenAll 方法會以非同步方式來等候多個 Task 或 Task 物件完成,因此,與執行緒的開發設計不同,使用工作可以有更多的方法與技巧,可以等候部分或者全部工作完成後,才會繼續進行處理其他程式碼。
這是執行結果
Press any key for continuing...

http://www.msn.com/zh-tw/ 執行緒 ID 5
https://world.taobao.com/ 執行緒 ID 6
https://tw.yahoo.com/ 執行緒 ID 3
https://www.microsoft.com 執行緒 ID 4
https://www.microsoft.com 的內容大小為 1020 bytes
https://world.taobao.com/ 的內容大小為 220949 bytes
https://www.microsoft.com 花費時間為 268.2176 ms
https://world.taobao.com/ 花費時間為 268.6948 ms
http://www.msn.com/zh-tw/ 的內容大小為 46066 bytes
http://www.msn.com/zh-tw/ 花費時間為 273.3024 ms
https://tw.yahoo.com/ 的內容大小為 192192 bytes
https://tw.yahoo.com/ 花費時間為 447.7144 ms
抓取四個網站內容 花費時間為 509.5968 ms
Press any key for continuing...
底下是完整的測試程式碼
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)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        Task t1 = Task.Factory.StartNew(() =>
        {
            GetWebContent(Urls[0]);
        });
        Task t2 = Task.Factory.StartNew(() =>
        {
            GetWebContent(Urls[1]);
        });
        Task t3 = Task.Factory.StartNew(() =>
        {
            GetWebContent(Urls[2]);
        });
        Task t4 = Task.Factory.StartNew(() =>
        {
            GetWebContent(Urls[3]);
        });

        Task.WhenAll(t1, t2, t3, t4).ContinueWith(t =>
        {
            sw.Stop();
            Console.WriteLine($"抓取四個網站內容 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
            Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
        });

        //// 這裡是另外一種做法
        //Task.WhenAll(t1, t2, t3, t4).ContinueWith(t =>
        //{
        //}).Wait();
        //sw.Stop();
        //Console.WriteLine($"抓取四個網站內容 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
        //Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");


        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");
    }
}

2017年9月28日 星期四

C# : 泛型 Generic 類別的物件序列化

當我們使用 C# 進行開發專案的時候,通常需要把當時物件的狀態記錄下來,也許是下次啟動應用程式時候,再把這個物件重新建立起來,並且回原成為原來的狀態值;或者是在這個程式的別的地方,需要再度重顯呈現出這個物件。
基於這樣的需求,我們可以把這個物件序列化成為別的格式,在這裡,我們會把這個測試物件序列化成為 JSON 格式內容;我們將會透過 Newtonsoft.Json 這個 NuGet 套件,把 C# 中的指定物件序列化成為 JSON 定義,也可以透過這個套件,將 JSON 內容,反序列化成為一個新的物件。
透過這樣的方法,您可以完成複製一個物件的功能
在這個練習中,我們將會使用到底下兩個類別,其中,我們將會建立 MyJSONClass<T> 這個泛型型別的物件,進行 JSON 序列與反序列化操作,而這個類別裡面會有兩個方法,分別將會提供這樣的功能。
public class MyClass
{
    public int ID { get; set; }
    public string Name { get; set; }
    public MyClass(int id, string name)
    {
        ID = id;
        Name = name;
    }
}

public class MyJSONClass<T>
{
    public T MyObject { get; set; }
    public string Content { get; set; }
    public MyJSONClass(T myObject)
    {
        MyObject = myObject;
    }
    public void Serialize()
    {
        Content = JsonConvert.SerializeObject(MyObject);
    }
    public T Deserialize()
    {
        return JsonConvert.DeserializeObject<T>(Content);
    }
}

進行測試

我們將使用底下程式碼進行測試
首先,我們建立一個 MyJSONClass<MyClass> 泛型類別物件 myJSONClassObject1,並且把這個物件內的屬性進行初始化設定。
接著,我們呼叫這個類別的序列化方法,就會得到物件序列化後的 JSON 內容,我們在這裡也將當時物件的 HashCode 也列印出來,這樣用來區分在進行反序列化動作之後,是否會得到另外一個記憶體空間的全新物件。
最後,我們使用該類別的反序列化方法,得到一個新的物件,並且,同樣的把這個物件的 HashCode 也列印出來,證實,我們確實得到一個新的物件。
MyJSONClass<MyClass> myJSONClassObject1 =
    new MyJSONClass<MyClass>(new MyClass(100, "Vulcan"));
myJSONClassObject1.Serialize();
MyClass myClassObject1 = myJSONClassObject1.MyObject;

Console.WriteLine($"JSON:{myJSONClassObject1.Content} / ID: {myClassObject1.ID} / HashCode: {myClassObject1.GetHashCode()}");
Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
Console.ReadKey();

MyClass myClassObject2 =
    myJSONClassObject1.Deserialize();
myClassObject1.ID = 999;
Console.WriteLine($"JSON:{myJSONClassObject1.Content} / ID: {myClassObject1.ID} / HashCode: {myClassObject1.GetHashCode()}");
Console.WriteLine($"ID={myClassObject2.ID}, Name={myClassObject2.Name} / HashCode: {myClassObject2.GetHashCode()}");
Console.ReadKey();
執行結果內容
JSON:{"ID":100,"Name":"Vulcan"} / ID: 100 / HashCode: 44223604
Press any key for continuing...

JSON:{"ID":100,"Name":"Vulcan"} / ID: 999 / HashCode: 44223604
ID=100, Name=Vulcan / HashCode: 62468121

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] 的使用方式