.NET Core 取消令牌:CancellationToken

在 .NET 開發中,CancellationToken(取消令牌)是一項比較重要的功能,掌握并合理的使用 CancellationToken 可以提升服務的性能。特別在異步編程中,我常常會以創建 Task 的方式利用多線程執行一些耗時或非核心業務邏輯,表面上看既提高了整個流程的執行速度,又充分利用了服務器資源。然而類似 Task 的方式如果沒設置過取消令牌,一旦開啟,是無法被外部取消的,所以當主線程出異常或被提前終止時,已開啟的異步線程其實依然在執行,這時對服務器資源可能是一種浪費,而 CancellationToken 就可以對這類情況進行一定的補救。

下面通過幾種常見的使用場景來介紹 CancellationToken。

在 HttpClient 中的使用

HttpClient 是開發中比較常用的一個組件,關于超時可通過 Timeout 參數進行設置,其實它也是可以通過配置 CancellationToken 來實現超時定義,使用 CancellationToken 的最大好處是調用鏈共享此令牌狀態,狀態變更時會自動做出響應。

public async Task<string> GetHomeAsync(CancellationToken cancellationToken = default)
{
  var client = _httpClientFactory.CreateClient();
  var response = await client.GetAsync("https://github.com/", cancellationToken);
  response.EnsureSuccessStatusCode();
  return await response.Content.ReadAsStringAsync();
}
public async Task<string> GetGithubHome()
{
  var cts = new CancellationTokenSource(1000);
  var result = await _githubService.GetHomeAsync(cts.Token);
  return result;
}

Github 一般訪問會比較慢,可通過設置 1s 演示效果:

在 gRPC 中的使用

通過 VS 的 gRPC 服務模板創建一個 gRPC 服務端(如果對 gRPC 的使用還不太了解,參考官方文檔 玩起來吧),服務端主要提供一個獲取用戶列表 (GetList) 的接口。實現如下,_userRepository 內部是基于 MongoDB 實現的查詢用戶數據,對應使用的 MongoDB.Driver 提供的方法默認已支持設置 CancellationToken,所以這里直接引用 ServerCallContext 上下文中的 CancellationToken,而此 CancellationToken 又是從客戶端傳遞來的,所以 CancellationToken 將作用于整個調用鏈中。另外如果在客戶端動態取消了此令牌,服務器也將會收到通知。

public override async Task<GetListReply> GetList(GetListRequest request, ServerCallContext context)
{
  await Task.Delay(1000); // 模擬效果,服務端停1s
  var users = await _userRepository.GetListAsync(context.CancellationToken);
  var reply = new GetListReply();
  foreach (var item in users)
  {
    reply.Users.Add(new UserModel { UserId = item.UserId, Name = item.Name });
  }
  return reply;
}

Client 端主要代碼如下,在接口層創建了 CancellationTokenSource 對象,并設置了令牌的過期時間,即在發起遠程調用時,如果 1s 內沒返回,那就取消這個調用。

public class UserService : IUserService
{
  private readonly UserClient _client;

  public UserService()
  {
    var channel = GrpcChannel.ForAddress("https://localhost:5001");
    _client = new UserClient(channel);
  }

  public async Task<GetListReply> GetListAsync(CancellationToken cancellationToken = default)
  {
    return await _client.GetListAsync(new GetListRequest(), cancellationToken: cts.Token);
  }
}
[HttpGet]
public async Task<string> GetUserList()
{
  var cts = new CancellationTokenSource(1000);
  var result = await _userService.GetListAsync(cts.Token);
  return JsonConvert.SerializeObject(result.Users);
}

在 WebAPI 中的使用

前端調用后端的接口一般是基于 Ajax 來實現,當瀏覽器網頁被 連續 F5 刷新頁面加載中被停止Ajax 請求被主動 abort 時,控制臺 network 看板中會出現一些狀態為 canceled 的請求,如下:

對于這類請求,客戶端雖然主動放棄了,如果服務端沒有相應處理,其實接口對應的后端程序還是在不停的執行,只是這個執行結果不會被使用而已,所以這其實是非常浪費服務器資源的。

實際上瀏覽器取消請求時,服務端會將 HttpContext.RequestAborted 中的 Token 綁定到 Action 的 CancellationToken 參數。我們只需在接口中增加參數 CancellationToken,并將其傳入其他接口調用中,程序識別到令牌被取消就會自動放棄繼續執行。

[HttpGet]
public async Task<string> Index(CancellationToken cancellationToken)
{
  try
  {
    await _userService.GetListAsync(cancellationToken);
    await Task.Delay(5000); // 等待期間,取消請求(Postman 即可模擬)
    await _githubService.GetHomeAsync(cancellationToken);
  }
  catch (Exception ex)
  {
    Console.WriteLine(ex.Message + Environment.NewLine + ex.StackTrace);
  }

  return "ok";
}

對于 WebAPI 接口被取消調用的場景,特別是對于查詢功能的接口,CancellationToken 的傳遞就顯得尤為必要了,它能減少很多底層服務接口的無效調用。

最后針對取消令牌產生的異常需要收尾干凈,一般像 WebAPI 項目可以使用自帶的過濾器或具有 AOP 功能的組件,gRPC 服務可使用自帶的攔截器功能。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371