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



2019年3月1日 星期五

等待 wait 工作 Task 物件的非同步方法在不同時間點拋出例外異常

等待 wait 工作 Task 物件的非同步方法在不同時間點拋出例外異常

當我們建立一個非同步工作的時候,這個非同步工作將有機會在不同的時間點拋出例外異常,此時,若我們使用了 Task.Wait() 方法來等待這個工作完成,那麼會有甚麼情況發生了?例如,非同步工作尚未開始之前,就直接拋出例外異常、在非同步方法執行期間,但是在工作等待前,就在非同步方法內拋出例外異常,此時當在等待此工作的時候,會有甚麼結果、最後,當非同步方法處理時間較長,而主執行緒早早就在等待此工作的完成,此時,非同步工作拋出例外異常,整個應用程式會發生甚麼問題
為了解答這些疑問,我們將要建立這樣的非同步方法,在主執行緒中,當啟動這個非同步方法之後,將會休息 1000 ms,而在非同步方法內,將會有底下幾個時間點拋出例外異常 Exception
  • 呼叫非同步方法的時候且尚未執行非同步任何程式碼的時候,立即拋出例外異常
  • 非同步方法執行 500ms 之後,拋出例外異常
  • 非同步方法執行 2000ms 之後,拋出例外異常

呼叫非同步方法的時候且尚未執行非同步任何程式碼的時候,立即拋出例外異常

我們建立一個 MyAsync 非同步方法,該非同步方法將會回傳一個工作 Task 物件,此時,在主執行緒內,將會休息 1000 ms 的時間,接者,使用 Wait() 方法等待 MyAsync 非同步方法的完成。
此時,該 MyAsync 非同步方法將會立即使用 Task.FromException 回傳一個工作物件,且該工作處於一個拋出例外異常的狀態,也就是該工作的 IsFaulted = true
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        Task task = MyAsync();
        Thread.Sleep(1000);
        task.Wait();
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    static Task MyAsync()
    {
        return Task.FromException(new Exception("非同步工作尚未開始,模擬拋出例外異常"));
    }
}
當執行上述範例程式,一旦當主執行緒休息 1000ms 後,接著使用 Wait() 方法等候該非同步方法完成的時候,將會得到底下的例外異常:非同步工作尚未開始,模擬拋出例外異常。
當然,該應用程式將會異常終止結束執行
Console
System.AggregateException
  HResult=0x80131500
  Message=One or more errors occurred.
  Source=System.Private.CoreLib
  StackTrace: 
   at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
   at System.Threading.Tasks.Task.Wait()
   at ConsoleApp2.Program.Main(String[] args) in D:\Vulcan\Projects\ConsoleApp2\ConsoleApp2\Program.cs:line 14

內部例外狀況 1:
Exception: 非同步工作尚未開始,模擬拋出例外異常

非同步方法執行 500ms 之後,拋出例外異常

我們建立一個 MyAsync 非同步方法,該非同步方法將會回傳一個工作 Task 物件,此時,在主執行緒內,將會休息 1000 ms 的時間,接者,使用 Wait() 方法等待 MyAsync 非同步方法的完成。
此時,該 MyAsync 非同步方法將會進入到非同步處理狀態,這裡我們使用的是 Task.Run 來取得執行緒集區內的一個執行緒,進行多工處理;而在該新執行緒內,將會休息 500ms (使用 Thread.Sleep(500) 方法) 接著拋出例外異常 (使用 SetException 方法)
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        Task task = MyAsync();
        Thread.Sleep(1000);
        task.Wait();
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    static Task MyAsync()
    {
        TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
        Task.Run(() =>
        {
            Thread.Sleep(500);
            tcs.SetException(new Exception("非同步工作開始,在等待工作前,模擬拋出例外異常"));
        });
        return tcs.Task;
    }
}
當執行上述範例程式,一旦當主執行緒休息 1000ms 後,接著使用 Wait() 方法等候該非同步方法完成的時候,此時主執行緒將會進入到封鎖 Block 狀態下,也就是主執行緒在 1000ms 之前,是無法處理任何事情的,而在非同步方法內,將會使用 Task.Run 進行非同步處理,並且在 500ms 之後,使用 TaskCompletionSource.SetException 方法,設定該非同步工作有例外異常產生,此時,將會得到底下的例外異常:非同步工作開始,在等待工作前,模擬拋出例外異常。
當然,該應用程式將會異常終止結束執行
Console
System.AggregateException
  HResult=0x80131500
  Message=One or more errors occurred.
  Source=System.Private.CoreLib
  StackTrace: 
   at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
   at System.Threading.Tasks.Task.Wait()
   at ConsoleApp2.Program.Main(String[] args) in D:\Vulcan\Projects\ConsoleApp2\ConsoleApp2\Program.cs:line 14

內部例外狀況 1:
Exception: 非同步工作開始,在等待工作前,模擬拋出例外異常

非同步方法執行 2000ms 之後,拋出例外異常

我們建立一個 MyAsync 非同步方法,該非同步方法將會回傳一個工作 Task 物件,此時,在主執行緒內,將會休息 1000 ms 的時間,接者,使用 Wait() 方法等待 MyAsync 非同步方法的完成。
此時,該 MyAsync 非同步方法將會進入到非同步處理狀態,這裡我們使用的是 Task.Run 來取得執行緒集區內的一個執行緒,進行多工處理;而在該新執行緒內,將會休息 2000ms (使用 Thread.Sleep(2000) 方法) 接著拋出例外異常 (使用 SetException 方法)
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        Task task = MyAsync();
        Thread.Sleep(1000);
        task.Wait();
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    static Task MyAsync()
    {
        TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
        Task.Run(() =>
        {
            Thread.Sleep(2000);
            tcs.SetException(new Exception("非同步工作開始,在等待工作執行之後,才模擬拋出例外異常"));
        });
        return tcs.Task;
    }
}
當執行上述範例程式,一旦當主執行緒休息 1000ms 後,接著使用 Wait() 方法等候該非同步方法完成的時候,此時主執行緒將會進入到封鎖 Block 狀態下,也就是主執行緒在 1000ms 之前,是無法處理任何事情的,而在非同步方法內,將會使用 Task.Run 進行非同步處理,並且在 2000ms 之後,使用 TaskCompletionSource.SetException 方法,設定該非同步工作有例外異常產生,這裡是要模擬當主執行緒在等待非同步方法完成,不過,非同步方法需要就久的時間才能夠完成處理的情境,此時,將會得到底下的例外異常:非同步工作開始,在等待工作執行之後,才模擬拋出例外異常。
+

當然,該應用程式將會異常終止結束執行
Console
System.AggregateException
  HResult=0x80131500
  Message=One or more errors occurred.
  Source=System.Private.CoreLib
  StackTrace: 
   at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
   at System.Threading.Tasks.Task.Wait()
   at ConsoleApp2.Program.Main(String[] args) in D:\Vulcan\Projects\ConsoleApp2\ConsoleApp2\Program.cs:line 14

內部例外狀況 1:
Exception: 非同步工作開始,在等待工作執行之後,才模擬拋出例外異常

結論

當使用工作類別來設計出非同步方法的時候,不論該非同步方法在甚麼時間點拋出例外異常,只要呼叫該非同步方法的執行緒使用 Wait 方法來等待該工作,這個時候,此應用程式將會終止執行。