最近在寫 Asp.Net Core 的 Api 時遇到一個非同步的問題,在解決之後真的覺得出乎意料,所以寫下來跟大家分享一下,以及避免以後再遇到。
甚麼是非同步
在解釋甚麼是非同步之前先讓大家認識一下同步的定義是甚麼,簡單說就是一個任務個任務接續的做下去,在做完前一個任務前不會做下一個任務,簡單來說如下圖所示。
flowchart LR
A(TASK A) --> B(TASK B)-->C(TASK C) --> D(TASK D) --> E(TASK E)
至於非同步來說就是可以有多個任務同時進行,不會因為同時進行的任務尚未完成而需要等待的狀況,就如下圖所示。
flowchart LR
A(TASK A) --> B(TASK B)
C(TASK C) --> D(TASK D) --> E(TASK E)
F(TASK F) --> G(TASK G)
所以遇到甚麼問題
遇到的問題是在於遇到高併發的請求時, Api 都會已逾時來反應這個請求,經過一番的修改以及交叉比對後發現,已同步的寫法來撰寫 Api 遇上需要與其他 Api 溝通時如果在高併發的狀況下會造成我們的 Api 再也沒有反應的狀況,再來就是當以錯誤的非同步寫法下也會造成嚴重的逾時狀況發生,所以接下來再進一步的說明。
Asp.Net Core 非同步的寫法
在 .Net Framework 4.5
之後多了 async
跟 await
的語法糖,只要在想要作為非同步的方法回傳值前宣告 async
並搭配 Task<T>
即可完成非同步方法的起手式,接下來如果是要同步等待結果的方法呼叫前使用 await
即可完成,若是不需等待時就不要加 await
的關鍵字即可。完整例子如下列所式。
public async Task<IEnumerable<string>> GetStringAsync()
{
await Task.Delay(TimeSpan.FromSeconds(10));
return new string[]{"First", "Second", "Third"};
}
遇到的問題
如果是從 .Net Framework 4.5
之前寫上來的朋友們可能會寫出同步的語法,但是同步的語法遇到高併發的請求再加上本身需要複雜的運算或式需要與其他 Api 溝通時就會造成本身的阻塞,甚至於導致於整個網站沒有回應的窘境。
再來同步的 Api 呼叫到非同步的語法時沒辦法使用 await
的關鍵字,所以只能使用 Task.Result
的方式來呼叫,如下圖所示。
[HttpGet("Result")]
public IEnumerable<string> GetResult()
{
return _service.GetStringAsync().Result;
}
不過同步的語法遇到高併發的請求會造成反應逾時的狀況,所以這時會使用非同步的語法來改寫,但是單純的加上 async
而搭配 Task.Result
還是會造成阻塞的狀況,這是爬文看到MSDN非同步程式設計才知道有這樣的結果,所以下列的寫法仍然會有造成請求阻塞的狀況。
[HttpGet("Result")]
public async Task<IEnumerable<string>> GetResult()
{
return _service.GetStringAsync().Result;
}
所以究竟該如何寫才是正確的寫法呢?
正確的寫法
所以綜合以上的內容,要寫一個非同步的 Api 基本上要先使用 async
這個關鍵字,接下來針對有沒有回傳值使用 Task<T>
或是 Task
的返還值,如下列所式。
[HttpGet("String")]
public async Task<IEnumerable<string>> GetString()
{
await Task.Delay(TimeSpan.FromSeconds(10));
return new string[]{"First", "Second", "Third"};
}
除此之外,遇到需要阻塞的方法時,也就是遇到需要等待其他方法的結果時要使用 await
的關鍵字,如下列所式,
[HttpGet("Result")]
public async Task<IEnumerable<string>> GetResult()
{
return await _service.GetStringAsync();
}
這裡千萬注意不要使用 Task.Result
這樣的寫法否則運行的反應時間會大打折扣 。
測試的結果
不論是以同步的寫法,或是在非同步的 Api 中呼叫 Task.Result
的寫法,當遇到每秒100次的高併發呼叫時的結果都會以超過五秒以上的延遲來反應,只有用 async / await
的寫法可以在三秒內正常反應。
結論
經過這次的經驗在撰寫 Api 時就先把 async
先寫好,如此可以避免高併發時造成 Api 的反應被阻塞住導致於後面的工作都無法進行的窘境,再來就是 非同步
的寫法都要以 aync
及 await
來撰寫,避免寫出 Task.Result
的寫法,如此可以避免阻塞的狀況發生。