繼續練習寫 ASP.NET Core 的正確姿勢。

除了在 ASP.NET Core 無所不在的 DI,另一個跨進 .NET Core 要習慣的轉變是 - async/await 非同步化! 延伸閱讀: ASP.NET async 基本心法

.NET 已逐漸走向非同步化。以 ViewComponent 介面為例,規格便要求檢視元件方法Task<IViewComponentResult> InvokeAsyncIViewComponentResult Invoke 二者擇一,開發者可以選擇寫非同步版 InvokeAsync 或是同步化版本 Invoke。我選擇過去較熟悉的同步寫法,但寫起來不順,原因是新一代的 HttpClient 元件提供的方法全是 async 版!

雖然加個 .Result 就可以將非同步方法轉為同步化(註:更完美的做法是 GetAwaiter().GetResult()),但這讓我產生自己正在抵抗潮流的不安感,還是把 Invoke 改寫成 InvokeAsync 吧!

第一步,先將 WeatherBlockViewComponent 的 Invoke() 改成 public async Task<IViewComponentResult> InvokeAsync(string zoneName)

出現綠蚯蚓警告,提示我們 async 方法中缺少了 await,因此我們得把 IWeatherService.GetWeatherData 改成 async 版,呼叫時就能加上 await。

    public interface IWeatherService
    {
        //WeatherData GetWeatherData(string zoneName);
        Task<WeatherData> GetWeatherDataAsync(string zoneName);
    }

介面修改了,實作 IWeatherService 的 SimpleWeatherService 要跟著修改:

public Task<WeatherData> GetWeatherDataAsync(string zoneName)
{
    return GetWeatherFromOpenDataApi(zoneName);
}

public async Task<WeatherData> GetWeatherFromOpenDataApi(string zoneName)
{
    var resp = await httpClient.GetAsync(openDataApiUrl);
    resp.EnsureSuccessStatusCode();
    var json = await resp.Content.ReadAsStringAsync();       
    using (var doc = JsonDocument.Parse(json,
        new JsonDocumentOptions { AllowTrailingCommas = true }))
    //...以下省略

GetWeatherFromOpenDataApi 改回傳 Task<WeatherData> 並宣告為 async,內部呼叫 HttpClient .GetAsync()、ReadAsStringAsync() 可加 await。回頭再修改 WeatherBlockViewComponent 寫成 var data = await _weatherService.GetWeatherDataAsync(zoneName);,整串非同步化修改才完成。這個案例也算展示了 async 病毒式的感染性 - 一沾到 async,上下游都要跟著改。

public async Task<IViewComponentResult> InvokeAsync(string zoneName)
{
    var data = await _weatherService.GetWeatherDataAsync(zoneName);
    return View(data);
}

執行結果不變,但骨子裡程式已經非同步化,在相同硬體環境可達到更高的產能

最後補充一個小訣竅 - 在 DI 練習中有個只傳回 new WeatherData() 的 FakeWeatherService,是道道地地的同步化程序,現在 IWeatherService 介面改成 async 版,純同步化程式要怎麼改成非同步化?

請愛用 Task.FromResult<T>:

public Task<WeatherData> GetWeatherDataAsync(string zoneName)
{
    return Task.FromResult<WeatherData>(new WeatherData
    {
        ZoneName = zoneName,
        Status = "冰雹龍捲風",
        MinTemp = "-18",
        MaxTemp = "38"
    });
}

要特別提醒,由於這個程序不包含任何非同步動作,純粹為了符合介面規格,FakeWeatherService 的 GetWeatherDataAsync 不用加上 async,也不要 await Task.FromResult,加了程式照樣可跑,但沒任何好處只浪費 100 Byte

Example of changing the methods in ASP.NET Core from sync to async.


Comments

# by Ike

「…我選擇過去較熟悉的非同步寫法…」 黑大是習慣「同步」還是「非同步」? 怎麼讀起來有點不順??

# by Jeffrey

to lke, (羞) 應該是同步才對。這些年,不摻幾個錯字我寫不出文章,又一次正常發揮 orz。錯字已修正。感謝提醒。

# by 水滸

其實我一直不是很理解非同步在運作的感覺 尤其是在Debug模式下進斷點,好像都看不出來?

# by sean.mars

請問黑大,最後的 `Task.FromResult<T>` 這種把同步改成非同步的作法,如果像是最一開始的 `InvokeAsync` 中 `IWeatherService` 還是使用同步但方法改用 async (就是 async 方法中沒有任何 await),這樣會造成什麼影響嗎? 前後兩者差別在哪? 謝謝

# by Jeffrey

to sean.mars,Task.FromResult<T>主要是用在原本只需傳回T,但介面規定回傳Task<T>的場合。不很確定你所說「使用同步但方法改用 async」的意思,能否提供一段範例程式?

# by Jeffrey

to 水滸,這篇文章 https://blog.darkthread.net/blog/common-async-await-mistakes/ 提到的研討會影片有 async 背後原理解析,可以參考看看。

# by 凱大

關於非同步async感染性的問題 其實還是有解的 雖然不是完全消除 但可以解得很漂亮 因為 事實上在 非同步方法中一樣也可以呼叫方法 調用變數 所以callback function 就是一種可以暫時讓callback 中的做法以同步的方式繼續撰寫的做法 方法入口可以透過外部帶入或注入 另一個則是for property 為了避免有property 被包上 Task 可以選擇在非同步方法內部就將await 的結果先行assign 到指定的 property上 如此一來當await 結束後 property上就是原本期望的型別 實際上沾上Task 可能是乘載property的 誤建 或是 更外層的東西

# by sean.mars

Hi, 黑大,裡如下面兩段程式碼,基本上結果應該都一樣,但實質上到底差在哪邊? GetSumA 的寫法會有何影響嗎? 謝謝 ```csharp public async Task<int> GetSumA(int x, int y); { return x + y; } private async Task<int> GetSumB(int x, int y) { return await Task.FromResult(x + y); } ```

# by Jeffrey

to sean.mars, 兩種寫法都合法可執行,但都不是良好寫法。加 async 程式會被編譯器轉換成一個 IAsyncStateMachine 類別(執行檔大小增加約 100 Bytes) 參考:https://blog.darkthread.net/blog/common-async-await-mistakes/ 若在其中沒用到 await 善用非同步特性提升 CPU 使用效率,意味著程式碼複雜化卻沒換來任何好處,形同浪費。 GetSumA() 寫法在 Visual Studio 會出現文章第二張圖的綠蚯蚓警告;GetSumB() 則如文末所提,加 async/await 只是白白讓程式碼變複雜,若不是介面要求,int GetSumb(int x, int y) { return x+y; } 就好;若介面要求 Task<int>,則不要加 async,回傳 Task.FromResult(x+y);

# by sean.mars

了解了,謝謝黑大

Post a comment