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


2019年2月4日 星期一

C# async 非同步方法需要額外花費多少時間成本之評估


C# async 非同步方法需要額外花費多少時間成本之評估

我們知道,當我們撰寫一個 async 方法的時候,就算我們只有撰寫一行 Console.Write 方法,編譯器也會幫我們產生出許多的 IL ( intermediate language ) 程式碼,也就是說,雖然我們僅有撰寫一行程式碼,面對一個非同步的方法,編譯器幫助我們產生了當要設計出一個非同步方法所需要用到的相關程式碼,這些程式碼主要是由一個狀態機 state machine 所組成,而且,編譯器會依據您正在設計的 async 方法,自動建立出其他的類別代碼;也就是說,當我們呼叫這個非同步方法的時候,將會透過編譯器所產生的程式碼來運作。

了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [Task Class] 的使用方式

其中第一個就是要進行狀態機的初始化設定工作,在狀態機內將會開始呼叫 MoveNext 方法,開始執行這個非同步方法;在這個狀態機內將會知道當呼叫 await 關鍵字的時候,並且當所呼叫的非同步方法執行完成之後,該繼續從哪裡來執行點,而狀態機內也會有我們所設計的非同步方法的敘述,不過,會依據您所設計的程式碼,拆解成為不同的區塊,但是,您不用擔心,狀態機將會把這些程式碼串接起來,並且也會處理當有例外異常發生的時候,把這些例外異常捕捉起來,記錄在工作物件內。
現在,我們要來看看當我們使用了 async 修飾詞之後,會有甚麼影響?這包含了在 async 方法內沒有使用 await 關鍵字與有使用 await 關鍵字的執行效能。
首先,我們來撰寫個同步方法的呼叫花費時間測試,測試程式碼如下:
在我的電腦上,螢幕輸出結果為 呼叫 1000 次 同步方法 的平均花費時間 : 0.0001087 ms ,好的,我們接下來繼續做下一個測試。
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < 1000; i++)
        {
            string foo = 同步方法();
        }
        sw.Stop();
        double cost = sw.Elapsed.TotalMilliseconds / 1000.0;
        Console.WriteLine($"呼叫 1000 次 同步方法 的平均花費時間 : {cost} ms");
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    static string 同步方法()
    {
        return "同步方法";
    }
}
我們來撰寫個空的非同步方法的呼叫花費時間測試,測試程式碼如下,在這裡的 空的非同步方法() 並沒有做任何事情,如同前面的同步方法,唯一的差異就是我們使用了 async 修飾詞在這個方法上,並也在這個 空的非同步方法() 內,也沒有使用任何的 await 關鍵字。
在我的電腦上,螢幕輸出結果為 呼叫 1000 次 同步方法 的平均花費時間 : 0.0014271 ms ,經過與前面的同步方法來比較,在同步方法前面加上 async 關鍵字之後,每次使用 await 還呼叫這個空的非同步方法,將會多花費 0.0013184ms 時間,也就是成長的 13.13倍,好的,我們接下來繼續做下一個測試。
C Sharp / C#
static async Task Main(string[] args)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < 1000; i++)
    {
        string foo = await 空的非同步方法();
    }
    sw.Stop();
    double cost = sw.Elapsed.TotalMilliseconds / 1000.0;
    Console.WriteLine($"呼叫 1000 次 同步方法 的平均花費時間 : {cost} ms");
    Console.WriteLine("Press any key for continuing...");
    Console.ReadKey();
}
static async Task<string> 空的非同步方法()
{
    return "同步方法";
}
現在,在空的非同步方法內,使用 await Task.Yield() 進行非同步方法的呼叫,讓我們來看看這樣的做法究竟會花費多少時間,測試程式碼如下:
其中,我們使用 await 等候的 Task.Yield 方法,其功能為: 您可以使用await Task.Yield();中非同步的方法,以強制以非同步方式完成的方法,我們是用來模擬一個等候非同步工作,但是又沒有要做甚麼事情的情境。
在我的電腦上,螢幕輸出結果為 呼叫 1000 次 同步方法 的平均花費時間 : 0.361438 ms ,很明顯的,這樣的做法比起空的非同步方法來說,多花費了 0.3600109 ms 時間,也就是成長了 253.27 倍之多。
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < 1000; i++)
        {
            string foo = await 立即返回非同步方法();
        }
        sw.Stop();
        double cost = sw.Elapsed.TotalMilliseconds / 1000.0;
        Console.WriteLine($"呼叫 1000 次 同步方法 的平均花費時間 : {cost} ms");
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    static async Task<string> 立即返回非同步方法()
    {
        await Task.Yield();
        return "同步方法";
    }
}
透過這樣的測試過程,同步方法的執行效能,比起非同步方法的執行效能,可以說來的快得多,可是,像要設計一個非同步方法,會相當的麻煩與不好維護程式碼,但是透過了 TPL & async & await 的幫助,可以讓我們使用同步程式碼設計邏輯,設計出具有非同步方法應用的成果,不過,我們所要付出的將會是一點點執行效能的犧牲。


了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [Task Class] 的使用方式