2019年1月16日 星期三

[點網] 非同步程式模型簡介

# 點網的異步程式模型

## 前言

從現在回頭看,還存在 .NET 裡面的非同步模型有三種:
* Event-based Asynchronous Model(EAM): methods + event handlers
* Asynchronous Programming Model(APM): Begin + End methods + IAsyncResult
* Task-based Asynchronous Programming(TAP) Model:Task object + (async/await)

在更早之前的就先跳過不討論。從這三種來看看差異在哪裡。

## Event-based Asynchronous Model(EAM)

第一種在 System.Net.WebClient 還看得到,例如,它使用 DownloadDataAsync 與 DownloadDataCompleted 來完成非同步操作。

```
public partial class Form1 : Form
{
    System.Net.WebClient webClient;
    public Form1()
    {
        InitializeComponent();
        webClient = new System.Net.WebClient();
        webClient.DownloadDataCompleted += WebClient_DownloadDataCompleted;
        webClient.DownloadDataAsync(new Uri("http://www.google.com"), "[form constructor trigger]");
        Debug.WriteLine("[form constructor]");
    }

    private void WebClient_DownloadDataCompleted(object sender, System.Net.DownloadDataCompletedEventArgs e)
    {
        Debug.WriteLine((string)e.UserState);
        Debug.WriteLine(BitConverter.ToString(e.Result));
    }
}
```
首先要先註冊 DownloadDataCompleted 發生時,要用自己寫的哪個函式來處理,範例中是用WebClient_DownloadDataCompleted 來接。當我們呼叫完 DownloadDataAsync 之後,就是等待事情做完,然後 WebClient_DownloadDataCompleted 會被某個 thread 呼叫執行,我們要做的事情就是在這裡寫好。如果有需要分辨不同需求的 DownloadDataAsync 所產生的 WebClient_DownloadDataCompleted,則可以把資訊放在 DownloadDataAsync 的第二個參數,由 e.UserState 拿回攜帶的資訊。
"[form constructor]" 在網頁內容之前就先寫出來,代表不是 blocking。

## Asynchronous Programming Model(APM)

第二種,則在新一點的物件中出現,像是 System.Net.WebRequest。例如,它使用 BeginGetResponse 與 AsyncCallback 這種 delegate callback 合體使用。

```
    public Form1()
    {
        InitializeComponent();
        System.Net.WebRequest webRequest = System.Net.WebRequest.Create("http://www.google.com");
        webRequest.BeginGetResponse(new AsyncCallback(
            (IAsyncResult ar) =>
            {
                System.Net.WebRequest req = (System.Net.WebRequest)ar.AsyncState;
                System.Net.WebResponse resp = req.EndGetResponse(ar);
                long cl = resp.ContentLength;
                byte[] buffer = new byte[cl];
                resp.GetResponseStream().Read(buffer, 0, (int)cl);
                Debug.WriteLine(BitConverter.ToString(buffer));
            }
            ), webRequest);
        Debug.WriteLine("[form constructor]");
    }
```
使用上,在 BeginGetResponse 把 callback 一同放進呼叫參數裡,我在這裡故意使用箭頭函式寫法,這樣可以顯出與 EAM 的不同。關注點就不同跳開在程式碼太遠的地方。當然也可以先準備好一個函式,呼叫 BeginGetResponse 時放進去。這樣的程式長像就會類似 EAM。像這樣,BeginGetResponse 與 callback 的對應就不容易錯亂。callback 攜帶資訊的地方在 BeginGetResponse 的第二個參數,callback 裡則用 ar.AsyncState 拿回來。

## Task-based Asynchronous Programming(TAP) Model

第三種又在新一點,它在 System.Net.Http.HttpClient 有用到。例如它使用 GetAsync 拿到 Task 物件,再利用 task 的操作拿到想要的內容。

```
    public Form1()
    {
        InitializeComponent();
        System.Net.Http.HttpClient httpClient;
        httpClient = new System.Net.Http.HttpClient();
        Task<System.Net.Http.HttpResponseMessage> taskHttpResponseMessage = httpClient.GetAsync("http://www.google.com");
        taskHttpResponseMessage.ContinueWith(new Action<Task<System.Net.Http.HttpResponseMessage>>(
            (Task<System.Net.Http.HttpResponseMessage> t) =>
            {
                System.Net.Http.HttpResponseMessage httpResponseMessage = taskHttpResponseMessage.Result;
                byte[] c = httpResponseMessage.Content.ReadAsByteArrayAsync().Result;
                Debug.WriteLine(BitConverter.ToString(c));
            }
            ));
        Debug.WriteLine("[form constructor]");
    }
```
在這裡,GetAsync 拿到 Task 物件,然後在 ContinueWith 裡使用箭頭函式處理呼叫成功之後該做什麼事。這樣子寫,程式看起來還像是 APM 的樣子,但是 Task 物件有自己攜帶 wait 之類的方法,在控制要不要 blocking 的時候更自由。

## Task-based Asynchronous Programming(TAP) Model + async/await

TAP 模式寫起來還是很像 APM 模式,多層疊或是流程長的話,程式碼也還是不容易看。但是在 async/await 修飾字出現之後,程式碼就好看很多。其原理僅是靠編譯器幫忙。由於 async/await 無法在 constructor 使用,我程式移到一個 getPage() 驅動。上面的例子就變得跟同步程式長得很像。

```
    public Form1()
    {
        InitializeComponent();
        getPage();
        Debug.WriteLine("[form constructor]");
    }

    async void getPage()
    {
        System.Net.Http.HttpClient httpClient;
        httpClient = new System.Net.Http.HttpClient();
        System.Net.Http.HttpResponseMessage httpResponseMessage = await httpClient.GetAsync("http://www.google.com");
        byte[] c = httpResponseMessage.Content.ReadAsByteArrayAsync().Result;
        Debug.WriteLine(BitConverter.ToString(c));
    }
```

## 總結

有時候會看到上面三述方法混和提供的物件,以後就可以按照需要自由取用。就功能來說這三種做法可以互相取代。當然以團隊來說,可以自己決定的話,就統一一種會比較好。

## 參考

* https://docs.microsoft.com/zh-tw/dotnet/standard/asynchronous-programming-patterns/index

* https://www.infoq.com/articles/Tasks-Async-Await

沒有留言:

張貼留言