2019年2月22日 星期五

EAP 事件架構非同步模式 Event-based Asynchronous Pattern 對應到 TAP 以工作為基礎的非同步模式

EAP 事件架構非同步模式 Event-based Asynchronous Pattern 對應到 TAP 以工作為基礎的非同步模式

當我們進行設計非同步應用程式碼的時候,除了要能夠讓程式碼進行非同步的多工處理,也需要針對非同步工作執行中,若發生了例外異常的時候,不會造成應用程式的崩壞,也需要能夠提供取消非同步方法的需求。在這篇文章中,將會針對使用 EAP 事件架構非同步模式 的 WebClient 這個類別,把這個類別提供的取消、發生例外異常的機制,轉換到 Task 物件之內。
想要設計一個非同步的工作,最為簡單的方式,那就是直接使用 Task.Run 方法,並且指定一個委派方法,如此,就會得到一個 Task 物件,開發者就可以使用 await 來等候這個非同步工作完成,不過,當這個非同步工作在執行中有例外異常發生或者有取消需求產生的時候,使用 await 等候的非同步工作的時候,會拋出一個例外異常出來,為了要讓整個應用程式不會崩潰,因此,需要使用 try...catch 敘述把 await 敘述包起來,針對所發生的非同步異常事件,分別進行處理。
首先,需要先把 WebClient 這個類別的非同步功能,打包成為一個 Task 工作為基礎的非同步工作,這裡,使用的是 TaskCompletionSource 這個類別,雖然這個非同步工作沒有任何回傳值,不過,當使用這個類別的時候,還是要指定一個泛型型別,在這個將會使用 object 型別,當這個非同步工作完成的時候,可以使用 tcs.SetResult(null); 設定這個非同步工作已經順利完成。
但是,當這個事件架構非同步應用發生了例外異常, WebClient 的 回呼 callback 委派方法內,將可以透過參數 DownloadStringCompletedEventArgs.Error 得到這次發生了甚麼例外異常,接下來就可以使用 TaskCompletionSource.SetException 方法,把 DownloadStringCompletedEventArgs.Error 屬性值傳遞到這個方法之內,如此,當使用 await 關鍵字 來等候這個非同步工作的時候,也就可會同樣的接收到這個例外異常。
對於取消功能,可以透過 WebClient.CancelAsync() 方法送出取消通知,在這個範例中,將會使用一個 Task.Run 來做到這件事情,這樣將會在另外一個執行緒下,先等候 2 秒鐘,接著,執行 WebClient.CancelAsync() 方法,取消這個非同步網路存取需求。如此,在 WebClient 的 callback 委派方法內,便可以透過 接著,在使用 DownloadStringCompletedEventArgs.Cancelled 屬性得知這次的 WebClient 存取過程中,是否有發生了取消請求,若有的畫,可以使用 TaskCompletionSource.SetCanceled() 方法,設定這個非同步工作屬於取消狀態。此時,當使用 await 等候這個非同步工作的時候,就會得到一個 TaskCanceledException 例外異常,如此,可以針對取消非同步工作事件發生之後,進行相關的程式碼狀態清除的作業。
C Sharp / C#
private static Task EAPtoTask(string url, bool needCancel)
{
    TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
    WebClient client = new WebClient();
    if (needCancel)
    {
        Task.Run(() =>
        {
            Console.WriteLine($"網路存取將會於2秒後取消");
            Thread.Sleep(2000);
            Console.WriteLine($"對 WebClient 送出取消");
            client.CancelAsync();
        });
    }
    client.Encoding = Encoding.UTF8;
    client.DownloadStringCompleted += (s, e) =>
    {
        if (e.Cancelled)
        {
            tcs.SetCanceled();
        }
        else if (e.Error != null)
        {
            Console.WriteLine($"喔喔,EAP 內的 callback 中得到有例外異常發生 {e.Error.Message}...");
            tcs.SetException(e.Error);
        }
        else
        {
            Console.WriteLine($"{e.Result}");
            tcs.SetResult(null);
        }
    };
    client.DownloadStringAsync(new Uri(url));
    return tcs.Task;
}
現在,來進行測試,首先,是要測試取消需求,當要存取這個 URL ( https://lobworkshop.azurewebsites.net/api/RemoteSource/Source1 )的時候,該 URL 將會超過 3 秒種以上的時間,才會回傳結果,不過,此時將會在啟動 WebClient 非同步需求後的 2 秒鐘後,就會對 WebClient 送出取消請求。現在,將會得到底下的執行結果:
Console
網路存取將會於2秒後取消
對 WebClient 送出取消
非同步工作發現到有取消 A task was canceled.
Press any key for continuing...
C Sharp / C#
try
{
    await EAPtoTask("https://lobworkshop.azurewebsites.net/api/RemoteSource/Source1", true);
}
catch (TaskCanceledException exCancellation)
{
    Console.WriteLine($"非同步工作發現到有取消 {exCancellation.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"非同步工作發現到有例外異常 {ex.Message}");
}
Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
接著要來呼叫 https://lobworkshop.azurewebsites.net/api/RemoteSource/SampleX Web API 服務,不過,這個 URL 並不存在,因此,當使用 WebClient 啟動非同步呼叫的時候,將會得到一個 WebException 例外異常,而執行結果如下:
Console
喔喔,EAP 內的 callback 中得到有例外異常發生 The remote server returned an error: (404) Not Found....
非同步工作發現到有例外異常 The remote server returned an error: (404) Not Found.
Press any key for continuing...
C Sharp / C#
try
{
    await EAPtoTask("https://lobworkshop.azurewebsites.net/api/RemoteSource/SampleX", false);
}
catch (TaskCanceledException exCancellation)
{
    Console.WriteLine($"非同步工作發現到有取消 {exCancellation.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"非同步工作發現到有例外異常 {ex.Message}");
}
Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
當要呼叫 https://lobworkshop.azurewebsites.net/api/RemoteSource/Sample 服務的時候,就可以正常取得遠端伺服器的回應文字內容,執行結果如下:
Console
來自遠端 ASP.NET Core Web API 服務的資料
Press any key for continuing...
C Sharp / C#
try
{
    await EAPtoTask("https://lobworkshop.azurewebsites.net/api/RemoteSource/Sample", false);
}
catch (TaskCanceledException exCancellation)
{
    Console.WriteLine($"非同步工作發現到有取消 {exCancellation.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"非同步工作發現到有例外異常 {ex.Message}");
}
Console.WriteLine("Press any key for continuing...");
Console.ReadKey();



2019年2月20日 星期三

.NET Framework 提供的非同步方法使用綜觀 : APM, EAP, Task, async / awa


.NET Framework 提供的非同步方法使用綜觀 : APM, EAP, Task, async / await

在這篇文章中,將會來快速瀏覽 Microsoft .NET Framework 開發框架內提供的各種不同非同步開發方法的使用方式 (例如:APM 非同步程式設計模型、EAP 事件架構非同步模式、TAP 以工作為基礎的非同步模式、async / await 的使用方式、把 AMP 程式轉換成為工作、把 EAP 程式轉換成為工作),在這裡將會要透過網路來讀取這個 URL https://lobworkshop.azurewebsites.net/api/RemoteSource/Sample 內的結果。

APM 非同步程式設計模型 Asynchronous Programming Model Pattern

當 .NET Framework 1.0 推出的時候,是沒有提供任何具體的非同步設計模式方法,而在 .NET Framework 1.1 的時候,推出了 APM 非同步程式設計模型 Asynchronous Programming Model Pattern。
在 APM 下所設計的非同步應用方法中,都會有 Beginxxx / Endxxx 這樣一對的方法,其中前面 Begin 開頭的方法,表示要啟動這個非同步呼叫,而後面的 End 開頭的方法,則表示要取得非同步方法的執行結果;不過,若當呼叫 End 開頭方法的時候,此時,非同步方法尚未執行完成,這個時候,呼叫 End 開頭方法的執行緒,將會進入到封鎖 Block 階段,不再繼續執行任何程式碼,一直等到非同步作業完成之後,才會繼續往下執行其他程式碼。
在這個例子中,使用 WebRequest.Create 來建立一個 WebRequest 物件,這個 WebRequest 類別有支援 APM & 存取一個 URL 資源的功能;因此,要啟動這個讀取遠端 Web 資源的時候,將會呼叫 request.BeginGetResponse 方法 (這裡的兩個參數,都傳進 null 空值,表示不需要使用 回呼 callback 機制來取得非同步最後執行結果),而當要取得 URL 資源內容的時候,就可以呼叫 EndGetResponse 方法。
這個範例執行結果如下所示:
Console
封鎖現在執行緒,取得結果
來自遠端 ASP.NET Core Web API 服務的資料
Press any key for continuing...
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        APM();
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    private static void APM()
    {
        WebRequest request = WebRequest.Create("https://lobworkshop.azurewebsites.net/api/RemoteSource/Sample");
        // 第一個參數也可以使用 null ,表示不使用 callback
        IAsyncResult ar = request.BeginGetResponse(null, null);
        Console.WriteLine($"封鎖現在執行緒,取得結果");
        HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(ar);
        Encoding enc = System.Text.Encoding.UTF8;
        StreamReader loResponseStream = new
          StreamReader(response.GetResponseStream(), enc);
        string Response = loResponseStream.ReadToEnd();
        Console.WriteLine($"{Response}");
    }
}
若在 APM 非同步設計模式下,想要透過 回呼 callback 來取得非同步方法執行結果,可以參考底下的範例程式碼,當要啟動非同步方法的時候,可以呼叫 BeginGetResponse 這個方法,不過,要傳入一個 callback 委派方法物件,這個委派方法將會當這個非同步方法執行完成之後,就會執行這個委派方法;而在 BeginGetResponse 方法內的第二個引數,將是要呼叫這個 回呼 callback 方法時候,要傳入進去的參數物件值。
這個範例執行結果如下所示:
Console
Press any key for continuing...
使用 回呼 callback 取得結果
來自遠端 ASP.NET Core Web API 服務的資料
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        APMCallback();
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    private static void APMCallback()
    {
        WebRequest request = WebRequest.Create("https://lobworkshop.azurewebsites.net/api/RemoteSource/Sample");
        // 第一個參數也可以使用 null ,表示不使用 callback
        IAsyncResult ar = request.BeginGetResponse(WebRequestCallback, (HttpWebRequest)request);
    }
    private static void WebRequestCallback(IAsyncResult ar)
    {
        Console.WriteLine($"使用 回呼 callback 取得結果");
        HttpWebRequest myHttpWebRequest = (HttpWebRequest)ar.AsyncState;
        HttpWebResponse response = (HttpWebResponse)myHttpWebRequest.EndGetResponse(ar);
        Encoding enc = System.Text.Encoding.UTF8;
        StreamReader loResponseStream = new
          StreamReader(response.GetResponseStream(), enc);
        string Response = loResponseStream.ReadToEnd();
        Console.WriteLine($"{Response}");
    }
}

EAP 事件架構非同步模式 Event-based Asynchronous Pattern

在 .NET Framework 2.0 的時候,改良了非同步程式設計模式,推出了 EAP 事件架構非同步模式 Event-based Asynchronous Pattern
在前面 APM 非同步設計模式中,有看到可以在 APM 中使用 回呼 callback 方法來取得非同步方法的執行結果;而在 EAP 非同步設計模式之中,將其簡化了 APM 的使用方式,不再需要使用 Beginxxx & Endxxx 這樣的方法,而是直接使用 callback 方法來取得非同步結果。
同樣的,當要取得網路上 URL 資源的時候,就可以使用 EAP 非同步設計模式來做到,這裡使用的是有支援 EAP 的 WebClient 類別。首先建立一個 WebClient 物件,接者綁定當非同步呼叫完成之後,要執行的 callback 委派方法,這裡使用的是: client.DownloadStringCompleted += Client_DownloadStringCompleted;,最後,就是使用 client.DownloadStringAsync(new Uri("https://lobworkshop.azurewebsites.net/api/RemoteSource/Sample")); 來啟動這個非同步方法。
當已經從遠端網站取得結果之後,Client_DownloadStringCompleted 委派方法就會被執行,而這個方法會有兩個參數,其參數用法與格式就如同 .NET Framework 框架中事件使用的方法相同,透過 DownloadStringCompletedEventArgs 參數,便可以取得這次非同步呼叫的處理結果。在這裡特別將呼叫 EAP 非同步方法前的執行緒 ID 與在 callback 方法內的執行緒 ID 顯示出來,可以看到,當執行 callback 委派方法的時候,使用的另外一個執行緒來執行,因此,取得非同步執行結果的時候,是不會造成呼叫非同步方法的執行緒處於被封鎖的狀態。
這個範例執行結果如下所示:
Console
EAP 執行前的的執行緒為 1
Press any key for continuing...
EAP 執行完成後的的 Callback 執行緒為 6
來自遠端 ASP.NET Core Web API 服務的資料
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        EAP();
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    private static void EAP()
    {
        WebClient client = new WebClient();
        Console.WriteLine($"EAP 執行前的的執行緒為 {Thread.CurrentThread.ManagedThreadId}");
        client.Encoding = Encoding.UTF8;
        client.DownloadStringCompleted += Client_DownloadStringCompleted;
        client.DownloadStringAsync(new Uri("https://lobworkshop.azurewebsites.net/api/RemoteSource/Sample"));
    }
    private static void Client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
    {
        Console.WriteLine($"EAP 執行完成後的的 Callback 執行緒為 {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine($"{e.Result}");
    }
}

TAP 以工作為基礎的非同步模式 Task-based Asynchronous Pattern

在 .NET Framework 4.0 的時候,設計出比起 APM / EAP 更加好用的 TAP 以工作為基礎的非同步模式 Task-based Asynchronous Pattern
TAP 是一個使用工作 Task 物件來設計非同步應用的設計模式,這是透過 TPL 工作平行程式庫 Task Parallel Library 所提供各項服務來實作出來的。同樣的,在這裡也需要設計出可以讀取相同 URL 的回傳結果,不過,在這裡使用的是工作來做到非同步執行結果。
在 .NET Framework 4.0 之後,建議要存取 Http 服務的時候,可以使用 HttpClient 這個類別物件,這是用來取代 .NET Framework 1.1 所推出的 WebClient 類別服務,當要呼叫遠端 Web API 服務的時候,可以使用 client.GetStringAsync("https://lobworkshop.azurewebsites.net/api/RemoteSource/Sample?a=123"); 敘述,啟動一個非同步呼叫,這個方法將會回傳一個 工作 Task 物件,這裡將會回傳一個泛型工作型別物件 Task<string>
當要取得結果的時候,可以透過 task.Result 屬性來取得這個非同步方法的執行結果,同樣的,當非同步方法尚未執行結束的時候,若要透過 task.Result 取得非同步執行結果的時候,將會造成呼叫非同步方法端的執行緒進入封鎖 Block 狀態,只要非同步方法執行完畢之後,該執行緒就會繼續執行下去;對於沒有回傳結果的 Task 物件,可以使用 task.Wait() 來等候這個非同步工作結束。另外,也可以透過 Task.ContinueWit 方法,建立一個 callback 方法,來取得非同步工作執行結果。
這個範例執行結果如下所示:
Console
封鎖現在執行緒,等候非同步工作結果...
來自遠端 ASP.NET Core Web API 服務的資料
Press any key for continuing...
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        TaskObject();
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    private static void TaskObject()
    {
        using (HttpClient client = new HttpClient())
        {
            Task<string> task = client.
                GetStringAsync("https://lobworkshop.azurewebsites.net/api/RemoteSource/Sample?a=123");
            Console.WriteLine("封鎖現在執行緒,等候非同步工作結果...");
            task.Wait();
            string result = task.Result;
            Console.WriteLine($"{result}");
        }
    }
}

async / await 關鍵字

在 .NET Framework 4.5 / C# 5.0,加入了兩個關鍵字, async / await ,讓程式設計師可以使用同步程式碼設計邏輯,設計出非同步的應用程式碼
TPL 工作平行程式庫 Task Parallel Library 已經提供了相當方便與好用的非同步開發環境,為了要能夠讓 .NET C# 開發者可以使用同步程式設計邏輯,設計出具有非同步應用的程式碼,因此,在 C# 5.0 的時候,推出了 修飾詞 async 與 關鍵字 await,透過 async 修飾詞設計出非同步的方法,與使用 await 關鍵字來等候非同步工作的完成,請注意,這裡是使用 等候 (await),而不是使用 等待 (wait),前者並不會造成呼叫非同步方法的時候,造成當前執行緒進入封鎖 Block 狀態,而後者將會造成當前執行緒進入封鎖狀態,一直到非同步工作完成為止。
這個範例同樣使用了 HttpClient 這個類別,不過,在呼叫非同步方法的時候,會使用 client.GetStringAsync("https://lobworkshop.azurewebsites.net/api/RemoteSource/Sample"); 來等候遠端 Web API 回傳最後結果內容,而因為在這個方法內有使用了 await 關鍵字,所以,這個方法 private static async Task AwaitTask() 需要加入 async 這個修飾詞,這樣,編譯器就會幫忙產生非同步方法會用到的相關程式碼,如此,讓我們設計的非同步應用程式碼,可以使用循序的同步方式來設計程式碼,並且不會造成執行緒封鎖,也節省了許多程式碼的撰寫。
這個範例執行結果如下所示:
Console
等候工作完成...
等候工作完成前的,現在執行緒為 1
等候工作完成後的,現在執行緒為 7
來自遠端 ASP.NET Core Web API 服務的資料
Press any key for continuing...
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        await AwaitTask();
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    private static async Task AwaitTask()
    {
        HttpClient client = new HttpClient();
        Task<string> task = client.GetStringAsync("https://lobworkshop.azurewebsites.net/api/RemoteSource/Sample");
        Console.WriteLine("等候工作完成...");
        Console.WriteLine($"等候工作完成前的,現在執行緒為 {Thread.CurrentThread.ManagedThreadId}");
        string result = await task;
        Console.WriteLine($"等候工作完成後的,現在執行緒為 {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine($"{result}");
    }
}

將 APM 設計成為 Task

由於使用 Task 工作物件來開發非同步應用程式碼相當的好用與方便,然而,對於之前使用 APM 寫的類別庫或者程式碼,也想要使用 工作 方式來設計與使用非同步應用,此時,可以使用 Task.Factory.FromAsync 這個工廠方法提供的功能,把 APM 類別,輕輕鬆鬆地打包成為 工作 應用的非同步程式碼。在這裡需要傳入 Beginxxx 委派方法、Endxxx 委派方法與要傳送過去的參數,在這個例子中,並沒有需要用到第三個參數,因此,可以傳送 null 過去即可。
Task.Factory.FromAsync 會回傳一個 Task<WebResponse> 物件,有了 Task 物件,便可以選擇使用 Task.Result 取得非同步運算的結果,或者使用 await 關鍵字來等候非同步運算的結果;在這個範例中,將是使用 await 關鍵字來等候 Task<WebResponse> 的結果。
這個範例執行結果如下所示:
Console
Press any key for continuing...
來自遠端 ASP.NET Core Web API 服務的資料
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        APMtoTask();
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    private static async Task APMtoTask()
    {
        WebRequest request = WebRequest.Create("https://lobworkshop.azurewebsites.net/api/RemoteSource/Sample");
        Task<WebResponse> task =
            Task.Factory.FromAsync<WebResponse>(request.BeginGetResponse, request.EndGetResponse, null);
        var response = await task;
        Encoding enc = System.Text.Encoding.UTF8;
        StreamReader loResponseStream = new
          StreamReader(response.GetResponseStream(), enc);
        string Response = await loResponseStream.ReadToEndAsync();
        Console.WriteLine($"{Response}");
    }
}

將 EAP 設計成為 Task

對於 EAP 相關的程式碼,也是可以重新包裝成為 Task 物件,在這裡,可以透過 TaskCompletionSource 類別來完成
這個範例執行結果如下所示:
Console
來自遠端 ASP.NET Core Web API 服務的資料
Press any key for continuing...
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        await EAPtoTask();
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    private static Task EAPtoTask()
    {
        TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
        WebClient client = new WebClient();
        client.Encoding = Encoding.UTF8;
        client.DownloadStringCompleted += (s, e) =>
        {
            Console.WriteLine($"{e.Result}");
            tcs.SetResult(null);
        };
        client.DownloadStringAsync(new Uri("https://lobworkshop.azurewebsites.net/api/RemoteSource/Sample"));
        return tcs.Task;
    }
}