[C#] async方法發送大量請求

Marvin Hsu
·
·
IPFS
·

最近工作上遇到一小段程式碼,一次要對外部的api發送大量的請求,偏偏量一大就常常發生拋出 A Task was cancelled 的例外,研究了一下大概是因為await跟GetAwaiter().GetResult()混用造成的,做了一個小小的sample放在Github上

https://reurl.cc/447Gr3

先看個範例:

////以執行緒封鎖的方式逐站取得Youbike站名
public IEnumerable<YoubikeEntity> GetYoubikeStation(string[] request)
{
     var taskList = request.Select(target => Task.Run(() => 
                    { 
                        return this.GetYoubikeStationBySNOAsync(target).GetAwaiter().GetResult(); })).ToArray();
                                                              return Task.WhenAll(taskList).GetAwaiter().GetResult();
}

在這邊先設定timeout=10秒鐘
結果如下,可以看到中間很多工作都被取消了:

再來改用沒有執行緒封鎖的方法

public async Task<IEnumerable<YoubikeEntity>> GetYoubikeStationAsync(string[] request)
        {
            var taskList = request.Select(target => this.GetYoubikeStationBySNOAsync(target));

            return await Task.WhenAll(taskList);
        }

程式很快速的就跑完了,並且沒有遇到惱人的timeout

兩種方法會有不一樣的結果,這是因為原來的方法其實底層是用await GetAsync(),但是上一層卻用.GetAwaiter().GetResult(),發送大量請求實,在執行到GetAsync()的時候執行緒會讓出來給下一個請求發送,然後執行緒就被占住了(傳說中的DeadLock?),這也是為什麼微軟會建議不要混用,不然驚喜總是出現在你意想不到的地方,然後下班時間要加班改資料

解決的方法有兩種,第一種就是徹底搞懂Task物件,這方面只要把微軟的非同步煮早餐範例做過一遍應該會有很大的幫助,再來就是另一個很hardcore的方法--把底層的GetAsync也lock住就不會有問題,只是程式會跑得很慢而已,我也有附在Github中。

CC BY-NC-ND 2.0 授权

喜欢我的作品吗?别忘了给予支持与赞赏,让我知道在创作的路上有你陪伴,一起延续这份热忱!

logbook icon
Marvin Hsu會把一些莫名其妙的事情全部串起來 https://marvinhsu.eth.link