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


沒有留言:

張貼留言