ASP.NET Core 練習 - 非同步化
10 |
繼續練習寫 ASP.NET Core 的正確姿勢。
除了在 ASP.NET Core 無所不在的 DI,另一個跨進 .NET Core 要習慣的轉變是 - async/await 非同步化! 延伸閱讀: ASP.NET async 基本心法
.NET 已逐漸走向非同步化。以 ViewComponent 介面為例,規格便要求檢視元件方法 需 Task<IViewComponentResult> InvokeAsync
或 IViewComponentResult 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
了解了,謝謝黑大