2019年4月7日 星期日

C# 如何設計具有事件 event 的回呼 callback 的類別

C# 如何設計具有事件 event 的回呼 callback 的類別

在這篇文章將會來說明如何設計出一個具有 事件 Event 和 回呼 Callback 委派機制的類別,這樣的類別是可以於值行一個非同步的工作,不過,透過這個類別的設計過程,將會了解到一些技術底層做法,因此將會有助於了解 C# async await 這類非同步工作的運作機制與相關除錯的問題。
首先,對於要提供一個具有 事件 Event 和 回呼 Callback 委派機制的類別,必須至少要提供兩個功能,第一個就是要啟動非同步工作的方法與當非同步工作完成之後要執行的委派事件方法。在這個範例中,將會建立一個 MyAsyncClass 類別,在這個類別中,有提供 DoRun() 方法,這個方法將會使用執行緒集區 ThreadPool 取得一個背景執行緒,並且會在這個背景執行緒中執行需要花費比較多時間才能夠完成事情,因此,一旦在執行緒 A 呼叫這個 DoRun() 方法之後,執行緒A便可以繼續執行其他的程式碼,而 DoRun() 方法內所產生的背景執行緒,將會並行執行中,也就是說,同時有兩個執行緒在執行中。
另外一個功能就是要提供的事件,這裡宣告 OnCompletion 的型別為 EventHandler,這也就是我們一般在 .NET 中使用的 事件 機制所用到的型別,而這個 OnCompletion 事件將會於 DoRun() 方法內的背景執行緒中用到;當背景執行緒內的程式碼都執行完成之後,便需要檢查呼叫這個非同步方法 (DoRun) 的呼叫端,是否有定義至少一個事件委派方法到 OnCompletion 公開欄位內,若有的話,將會需要執行這些委派方法,在此使用的 C# 敘述為 OnCompletion?.Invoke(this, EventArgs.Empty); 。在這裡要特別注意的是,執行 OnCompletion?.Invoke(this, EventArgs.Empty); 這個敘述的時候是在背景執行緒下,而不是在呼叫端的執行緒下,雖然該事件委派方法是定義在呼叫端的類別內,這並不代表非同步事件內,當非同步作業完成後,要執行的事件委派方法就會在呼叫端的執行緒內,理由很簡單,若沒有特別的同步機制存在,程式設計很難就 .NET 預設提供的功能內,做到在 執行緒B 內,可以指定一段程式碼,指定在執行緒A 下來執行,這是做不到的。
在呼叫端會先建立一個 CallbaskNThread 物件,接著使用 myAsyncObject.OnCompletion 事件欄位,搭配 Lambda 來定義當非同步事件完成後要執行的方法,最後,執行 DoWork 便開始進行非同步作業了。
底下是這個講解範例程式碼執行輸出結果,從這些內容將可以確認,事件委派方法並不是在主執行緒下來執行的。
Main方法內的執行緒ID=1
Main方法內的開始呼叫 DoRun 方法的執行緒ID=1
執行 DoRun 前的執行緒ID=1
Press any key for continuing...
進入到非同步執行緒內的執行緒ID=3
模擬需要3秒鐘的非同步工作
準備要呼叫 callback,現在的執行緒ID=3
在 Main 方法內的委派 callback ,現在的執行緒ID=3
callback 執行結束了
C Sharp / C#
namespace CallbackNThread
{
    class MyAsyncClass
    {
        public EventHandler OnCompletion;
        public void DoRun()
        {
            Console.WriteLine($"執行 DoRun 前的執行緒ID={Thread.CurrentThread.ManagedThreadId}");
            ThreadPool.QueueUserWorkItem(x =>
            {
                Console.WriteLine($"進入到非同步執行緒內的執行緒ID={Thread.CurrentThread.ManagedThreadId}");
                Console.WriteLine("模擬需要3秒鐘的非同步工作");
                Thread.Sleep(3000);

                Console.WriteLine($"準備要呼叫 callback,現在的執行緒ID={Thread.CurrentThread.ManagedThreadId}");
                OnCompletion?.Invoke(this, EventArgs.Empty);
            });
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"Main方法內的執行緒ID={Thread.CurrentThread.ManagedThreadId}");
            MyAsyncClass myAsyncObject = new MyAsyncClass();
            myAsyncObject.OnCompletion += (s, e) =>
            {
                Console.WriteLine($"在 Main 方法內的委派 callback ,現在的執行緒ID={Thread.CurrentThread.ManagedThreadId}");
                Thread.Sleep(1000);
                Console.WriteLine("callback 執行結束了");
            };
            Console.WriteLine($"Main方法內的開始呼叫 DoRun 方法的執行緒ID={Thread.CurrentThread.ManagedThreadId}");
            myAsyncObject.DoRun();

            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }
    }
}


2019年4月6日 星期六

C# 的 WhenAll 與 WaitAll 的差異在哪裡

C# 的 WhenAll 與 WaitAll 的差異在哪裡

當需要使用 Task 類別來建立出一群的 非同步工作 物件,使這些工作物件可以並行執行,在 C# 中,可以使用 Task.WaitAll() 方法或者使用 Task.WhenAll() 方法來等待這些工作的全部都完成。這兩種做法的第一個差異就是,Task.WaitAll() 使用封鎖當前執行緒,等待所有的非同步工作完成,而 Task.WhenAll() 可以使用 await 關鍵字來等待所有的非同步工作的完成,而在等待的時候,將會 return 到原先呼叫的方法內。
在這裡使用一個範例程式,建立起 15 個非同步工作,這些非同步工作將會使用 HttpClient 連線到 https://lobworkshop.azurewebsites.net/api/RemoteSource/Add/15/43 Web API 服務,要求將 15 / 43 相加起來,不過,最後一個參數則是指定這個兩個整數的相加計算需要暫緩多久的時間(以秒來計算),才會回傳結果回來。
在 Main() 方法內的 WaitAll() ,將會逐一把 sleepSeconds 的每個整數值取出來,並且組合成為一個新的 URL,使用 HttpClient 類別產生的物件,呼叫遠端的 Web API 服務。原則上,這些整數相加的工作都會指定在 1~7 秒鐘內完成。
所產生的 task 物件,將會儲存到 List> allTasks 變數內,最後,使用 Task.WaitAll(allTasks.ToArray()); 來進行執行緒封鎖的等待方式,等待所有的非同步工作執行完成。
而在在 Main() 方法內的 WhenAll() ,將會逐一把 sleepSeconds 的每個整數值取出來,並且組合成為一個新的 URL,使用 HttpClient 類別產生的物件,呼叫遠端的 Web API 服務。原則上,這些整數相加的工作都會指定在 1~7 秒鐘內完成。
所產生的 task 物件,將會儲存到 List> allTasks 變數內,最後,使用 await Task.WhenAll(allTasks.ToArray()); 來進行等待所有的非同步工作執行完成,底下是反覆執行三次的執行結果。
觀察這三次的執行結果可以得到 使用 Task.WhenAll() 方法所花費的時間小於使用 Task.WaitAll() 方法所花費的時間,不過,差距大約只有不到 0.5 秒
Wait total 7467 ms
Press any key for continuing...
Wait total 7147 ms
Wait total 7571 ms
Press any key for continuing...
Wait total 7139 ms
Wait total 7431 ms
Press any key for continuing...
Wait total 7138 ms
C Sharp / C#
namespace WaitAllWhenAll
{
    class Program
    {
        static List<int> sleepSeconds = new List<int>() { 3, 2, 5, 7, 2, 3, 4, 5, 5, 1, 7, 2, 4, 4, 5 };
        static void Main(string[] args)
        {
            WaitAll();
            WhenAll();


            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }

        private static async void WhenAll()
        {
            string host = "https://lobworkshop.azurewebsites.net";
            string path = "/api/RemoteSource/Add/15/43/@";
            string url = $"{host}{path}";

            Stopwatch sw = new Stopwatch();
            sw.Start();
            List<Task<string>> allTasks = new List<Task<string>>();
            foreach (var item in sleepSeconds)
            {
                var fooUrl = url.Replace("@", item.ToString());
                var task = new HttpClient().GetStringAsync(fooUrl);
                allTasks.Add(task);
            }
            await Task.WhenAll(allTasks.ToArray());
            sw.Stop();
            Console.WriteLine($"Wait total {sw.ElapsedMilliseconds} ms");
        }

        private static  void WaitAll()
        {
            string host = "https://lobworkshop.azurewebsites.net";
            string path = "/api/RemoteSource/Add/15/43/@";
            string url = $"{host}{path}";
            Stopwatch sw = new Stopwatch();
            sw.Start();
            List<Task<string>> allTasks = new List<Task<string>>();
            foreach (var item in sleepSeconds)
            {
                var fooUrl = url.Replace("@", item.ToString());
                var task = new HttpClient().GetStringAsync(fooUrl);
                allTasks.Add(task);
            }
            Task.WaitAll(allTasks.ToArray());
            sw.Stop();
            Console.WriteLine($"Wait total {sw.ElapsedMilliseconds} ms");
        }
    }
}



2019年4月5日 星期五

C# 的 await 與 wait 的差異在哪裡

C# 的 await 與 wait 的差異在哪裡

當在 C# 進行非同步 (Asynchronous) 工作程式設計的時候,通常會看到有兩種選擇來等候非同步工作的完成,第一種就是使用 Task 類別所提供的 Wait() 方法,另外一種就是使用 C# 5.0 之後所提供的 await 關鍵字。
當您想要在程式碼中使用 await 關鍵字,這個方法必須加上 async 修飾詞,並且當方法加上了 async 修飾詞之後,這個方法的回傳值僅能夠為 Task / Task / void 這三者其中之一,若宣告了其他的回傳型別,就會造成編譯時候產生錯誤訊息;例如,當宣告了 static async int My() { } 這樣的方法,將會從編譯器得到這樣的錯誤訊息: 錯誤 CS1983 非同步方法的傳回類型必須為 void、Task 或 Task<T>
首先,先來查看 await / wait 在劍橋字典上的定義,不論定義是甚麼,中文上可以翻譯成為等待或者等候,其實,從這樣的定義文字中,很難理解到在 C# 中這兩個用法的差異
to allow time to go by, especially while staying in one place without doing very much, until someone comes, until something that you are expecting happens or until you can do something
to wait for or be waiting for something
現在,撰寫一個測試範例小程式(如下所示),來了解 Wait() 方法的運作模式與特性;當程式執行到 Wait() 方法之後,當前的執行緒就進入到封鎖狀態,但是,這樣會有甚麼問題呢?因為 Wait() 方法需要等待非同步工作的執行完成,若非同步工作沒有執行完畢,回報最終執行結果前,這個呼叫 Wait() 方法的執行緒就無法執行其他程式碼,因為他需要不斷的了解,非同步工作執行完成了沒。
這個測試程式首先會執行 DoWait() 這個方法,在這個方法內,將會先執行 Before() 方法,顯示出一段訊息文字,接著就會使用 Wait() 方法來等候非同步工作的完成,這裡是使用 MyMethodAsync().Wait() 這個敘述,在這個敘述中,首先會執行 MyMethodAsync() 方法,而在這個 MyMethodAsync() 方法內,直接回傳了一個 Task.Run 所產生的非同步工作之 Task 物件。
現在,因為 MyMethodAsync() 方法回傳了一個 Task 物件,因此,就可以使用 Wait() 方法來等候這個非同步工作的完成。
在 Main 方法內,看到當呼叫完成 DoWait() 方法之後,將會每隔 0.5 秒的時間,顯示一個訊息。
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine($"主執行緒開始執行," +
            $"ID={Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine("呼叫 DoWait 方法");
        DoWait();
        for (int i = 0; i < 10; i++)
        {
            Thread.Sleep(500);
            Console.WriteLine($"主執行緒正在處理其他事情({(i + 1) * 500}ms) " +
            $"(執行緒:{Thread.CurrentThread.ManagedThreadId})");
        }
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    static void DoWait()
    {
        Before();
        MyMethodAsync().Wait();
        After();
    }
    private static void Before()
    {
        Console.WriteLine($"  [Before] 呼叫 MyMethodAsync().Wait(); 前的" +
            $"執行緒:{Thread.CurrentThread.ManagedThreadId}");
    }
    private static void After()
    {
        Console.WriteLine($"  [After] 呼叫 MyMethodAsync().Wait(); 後的" +
            $"執行緒:{Thread.CurrentThread.ManagedThreadId}");
    }
    static Task MyMethodAsync()
    {
        Console.WriteLine($"進入到 MyMethodAsync 前," +
            $"所使用的執行緒 :{Thread.CurrentThread.ManagedThreadId}");
        return Task.Run(() =>
        {
            Console.WriteLine($"MyMethodAsync 開始進行非同步工作," +
                $"所使用的執行緒 :{Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine("需要花費 7 秒鐘");
            Thread.Sleep(7000);
        });
    }
}
底下為這個測試範例程式的執行結果,原則上,當 呼叫 MyMethodAsync().Wait() 敘述執行之後,另外一個背景執行緒就已經並行執行了,而要能夠接續執行 MyMethodAsync().Wait() 之後的程式碼,唯一的條件那就是要等到這個背景執行緒(也就是設計的非同步工作)執行完畢後,才能夠繼續網頁執行下去。
因此,將會看到,只有非同步工作中的程式碼,使用的是 執行緒 3 來執行,而其他的程式碼都會在執行緒1下來執行。
Console
主執行緒開始執行,ID=1
呼叫 DoWait 方法
  [Before] 呼叫 MyMethodAsync().Wait(); 前的執行緒:1
進入到 MyMethodAsync 前,所使用的執行緒 :1
MyMethodAsync 開始進行非同步工作,所使用的執行緒 :3
需要花費 7 秒鐘
  [After] 呼叫 MyMethodAsync().Wait(); 後的執行緒:1
主執行緒正在處理其他事情(500ms) (執行緒:1)
主執行緒正在處理其他事情(1000ms) (執行緒:1)
主執行緒正在處理其他事情(1500ms) (執行緒:1)
主執行緒正在處理其他事情(2000ms) (執行緒:1)
主執行緒正在處理其他事情(2500ms) (執行緒:1)
主執行緒正在處理其他事情(3000ms) (執行緒:1)
主執行緒正在處理其他事情(3500ms) (執行緒:1)
主執行緒正在處理其他事情(4000ms) (執行緒:1)
主執行緒正在處理其他事情(4500ms) (執行緒:1)
主執行緒正在處理其他事情(5000ms) (執行緒:1)
Press any key for continuing...
好的,上述的測試範例小程式並不複雜,只需要以同步程式碼設計角度來理解,就能夠知道整個程式的運作流程,現在,要來了解 await 關鍵字的運作方式,並理解與 Wait() 方法有何不同。
現在,在 Main 方法內,將會有一個 DoAwait() 方法,所以,當程式碼執行到這個敘述的時候,將會進入到 DoAwait() 方法內。在其方法內,將會使用 await 關鍵字來等候 MyMethodAsync() 非同步工作的完成,使用的是這個 C# 敘述 await MyMethodAsync();
awiat 關鍵字與 Wait() 方法最大的差異點就在於,當使用後者來等待一個非同步工作,呼叫端的執行緒因為要知道非同步工作是否已經完成了,所以,會進入到封鎖狀態下,持續地獲得非同步工作已經完成了;而當使用 await 關鍵字,當要等候非同步工作執行完畢的狀態時候,將會立即 retun 回到呼叫這個方法 (是這個 DoAwait 方法) 的地方,也就是會回到 Main 方法內呼叫 DoAwait 方法的點,注意,此時,主執行緒是沒有進入到封鎖的狀態,因此,主執行緒將會繼續執行呼叫 DoAwait() 之後的敘述,也就是會每隔 0.5 秒的時間,顯示一個訊息。
當主執行緒每隔 0.5 秒顯示一個訊息,並且做 10 次,也就是說,在 5 秒鐘內,將會顯示出十個訊息文字。就在此時,背景執行緒卻還在持續執行非同步的工作,這個非同步工作將會需要 7 秒鐘的時間才能夠完成。
因此,當 10 個 Main 方法內顯示的訊息文字顯示完畢之後(使用的執行緒1來執行),非同步工作是還沒有完成的;而當非同步工作完成之後, await MyMethodAsync() 敘述之後的程式碼將會要繼續來執行,此時,將會呼叫 After() 方法。
現在,看到另外一個與呼叫 Wait() 方法的不同點,當呼叫 Wait() 方法之後,並且非同步工作執行完畢之後,呼叫 After() 方法,顯在是使用 執行緒1 這個執行緒來繼續執行;而在使用 await 關鍵字的時候,在 await 關鍵字之後的程式碼要繼續執行,在此是要呼叫 After() 方法,卻是得到了是使用 執行緒3 來繼續執行 await 之後的程式碼。
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine($"主執行緒開始執行," +
            $"ID={Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine("呼叫 DoAwait 方法");
        DoAwait();
        for (int i = 0; i < 10; i++)
        {
            Thread.Sleep(500);
            Console.WriteLine($"主執行緒正在處理其他事情({(i + 1) * 500}ms) " +
            $"(執行緒:{Thread.CurrentThread.ManagedThreadId})");
        }

        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }

    static async void DoAwait()
    {
        Before();
        await MyMethodAsync();
        After();
    }

    private static void Before()
    {
        Console.WriteLine($"  [Before] 呼叫 MyMethodAsync().Wait(); 前的" +
            $"執行緒:{Thread.CurrentThread.ManagedThreadId}");
    }

    private static void After()
    {
        Console.WriteLine($"  [After] 呼叫 MyMethodAsync().Wait(); 後的" +
            $"執行緒:{Thread.CurrentThread.ManagedThreadId}");
    }

    static Task MyMethodAsync()
    {
        Console.WriteLine($"進入到 MyMethodAsync 前," +
            $"所使用的執行緒 :{Thread.CurrentThread.ManagedThreadId}");
        return Task.Run(() =>
        {
            Console.WriteLine($"MyMethodAsync 開始進行非同步工作," +
                $"所使用的執行緒 :{Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine("需要花費 7 秒鐘");
            Thread.Sleep(7000);
        });
    }
}
Console
主執行緒開始執行,ID=1
呼叫 DoAwait 方法
  [Before] 呼叫 MyMethodAsync().Wait(); 前的執行緒:1
進入到 MyMethodAsync 前,所使用的執行緒 :1
MyMethodAsync 開始進行非同步工作,所使用的執行緒 :3
需要花費 7 秒鐘
主執行緒正在處理其他事情(500ms) (執行緒:1)
主執行緒正在處理其他事情(1000ms) (執行緒:1)
主執行緒正在處理其他事情(1500ms) (執行緒:1)
主執行緒正在處理其他事情(2000ms) (執行緒:1)
主執行緒正在處理其他事情(2500ms) (執行緒:1)
主執行緒正在處理其他事情(3000ms) (執行緒:1)
主執行緒正在處理其他事情(3500ms) (執行緒:1)
主執行緒正在處理其他事情(4000ms) (執行緒:1)
主執行緒正在處理其他事情(4500ms) (執行緒:1)
主執行緒正在處理其他事情(5000ms) (執行緒:1)
Press any key for continuing...
  [After] 呼叫 MyMethodAsync().Wait(); 後的執行緒:3