Dotnet線程取消的深度進(jìn)階
取消的概念
通常我們最熟悉的,是一個(gè)方法的中止。中止是完全的。一個(gè)方法中止了,則這個(gè)方法不再往下執(zhí)行,方法中前面已經(jīng)完成的部分會(huì)被拋棄,并返回一個(gè)設(shè)定的結(jié)果。
取消則不同。
通常,取消是由其它代碼發(fā)出的命令,也就是說(shuō),是由一些代碼去請(qǐng)求取消,另一部分代碼的響應(yīng)取消。而且,實(shí)際發(fā)生的情況,是請(qǐng)求代碼只是通知響應(yīng)代碼,希望它能停止執(zhí)行;響應(yīng)代碼會(huì)按照自己設(shè)定的方式對(duì)取消請(qǐng)求做出響應(yīng),有可能立即停止任務(wù),也有可能繼續(xù)運(yùn)行下去,直到一個(gè)可以停止的點(diǎn),甚至可能完全忽略這個(gè)取消請(qǐng)求。
概念清楚了,怎么做?
取消令牌
既然是一方請(qǐng)求,另一方響應(yīng),那對(duì)于響應(yīng)代碼來(lái)說(shuō),重要的是能夠知道并響應(yīng)取消請(qǐng)求。
在 Dotnet 里,給出了一個(gè)東西,叫取消令牌 ( Cancellation Tokens )。這個(gè)令牌,就是請(qǐng)求取消的載體。
請(qǐng)求代碼發(fā)起取消時(shí),實(shí)際是發(fā)起了一個(gè)對(duì)「取消令牌」的取消操作,然后,響應(yīng)代碼將對(duì)這個(gè)被取消的令牌做出正確反應(yīng)。
如果看到這兒有點(diǎn)混亂的話,看一下示例代碼:
async Task SomethingAsync(int data, CancellationToken cancellationToken)
{
var result = await FirstStepAsync(data, cancellationToken);
await SecondStepAsync(intermediateValue, cancellationToken);
}
響應(yīng)代碼基本都是這個(gè)樣子。這里面,CancellationToken 就是上面說(shuō)的取消令牌。
CancellationToken 可以在任何地方被設(shè)置為取消:用戶(hù)按下取消按鈕,或客戶(hù)端斷開(kāi)連接,超時(shí),等等。重要的是,當(dāng)它被設(shè)置為取消時(shí),就表示響應(yīng)代碼需要處理取消了。
注意:一個(gè) CancellationToken 只能被取消一次。一旦它被取消,就會(huì)永遠(yuǎn)保持取消狀態(tài)。
帶有取消令牌的方法定義
上面的示例,就是一個(gè)典型的帶有取消令牌的方法定義。
按照微軟的習(xí)慣,帶有 CancellationToken 的方法有以下約定:
- CancellationToken 通常是最后一個(gè)參數(shù)
- 方法通常會(huì)提供一個(gè)重載,或默認(rèn)參數(shù)值,以便調(diào)用者可以不提供取消令牌而直接調(diào)用
當(dāng)然,這是一個(gè)非強(qiáng)制的約定。如果你不介意別人看著別扭,可以不管這個(gè)約定。
看幾個(gè)例子:
Task SomethingAsync(int data) => SomethingAsync(data, CancellationToken.None);
async Task SomethingAsync(int data, CancellationToken cancellationToken)
{
...
}
async Task SomethingAsync(int data, CancellationToken cancellationToken = default)
{
...
}
在這里,CancellationToken 代表任何類(lèi)型或任何原因的取消。
通過(guò) CancellationToken 參數(shù),方法聲明了自己可以響應(yīng)取消。而實(shí)際上,這只是個(gè)聲明。代碼中,CancellationToken 可能會(huì)被忽略。因此,有這個(gè)聲明僅僅表示方法可能支持取消,而不是一定支持。
方法對(duì)取消的響應(yīng)
上面說(shuō)到了,響應(yīng)代碼可以響應(yīng)取消,也可以不取消。
而即使響應(yīng)代碼真的去響應(yīng)取消,通常也會(huì)有不同的情況。
通常來(lái)說(shuō),如果取消請(qǐng)求到達(dá)時(shí),響應(yīng)方法實(shí)際取消了一些工作,會(huì)拋出 OperationCanceledException 來(lái)通知調(diào)用程序;而如果取消被忽略,或者取消請(qǐng)求來(lái)的太晚而任務(wù)已經(jīng)完成,那響應(yīng)方法會(huì)正常返回,而且不拋出 OperationCanceledException 異常。這個(gè)在微軟的基礎(chǔ)類(lèi)庫(kù)(BCL)中,體現(xiàn)得很明顯。
大多數(shù)情況下,異常會(huì)被逐層傳出。再看一下上面的例子:
async Task SomethingAsync(int data, CancellationToken cancellationToken)
{
var result = await FirstStepAsync(data, cancellationToken);
await SecondStepAsync(intermediateValue, cancellationToken);
}
如果 FirstStepAsync 或 SecondStepAsync 拋出 OperationCanceledException,那這個(gè)異常也會(huì)從 SomethingAsync 中傳出給調(diào)用者。
這里要強(qiáng)調(diào)一下:看過(guò)很多代碼,在請(qǐng)求取消時(shí)會(huì)不拋出異常而直接返回。不要這樣做。調(diào)用者不知道這個(gè)取消是被接受,還是被忽略,會(huì)出大問(wèn)題的。
一個(gè)常見(jiàn)的錯(cuò)誤用法
在代碼 Review 時(shí),見(jiàn)過(guò)好幾次這樣的情況:
async Task SomethingAsync(CancellationToken cancellationToken)
{
var test = await Task.Run(() =>
{
...
}, cancellationToken);
...
}
// 注意,這個(gè)例子的寫(xiě)法是錯(cuò)的。
這個(gè)有必要專(zhuān)門(mén)拿出來(lái)說(shuō)一下。
很多人把委托和 CancellationToken 傳遞給 Task,期望在令牌取消時(shí)取消委托。注意,這個(gè)理解是錯(cuò)的。
Task.Run 是對(duì)線程池的委托調(diào)度,是一個(gè)立即完成的瞬時(shí)動(dòng)作。CancellationToken 在這兒的作用是取消調(diào)度這個(gè)動(dòng)作,而這個(gè)動(dòng)作是立即完成的,換句說(shuō)說(shuō),一旦走到這一行,調(diào)度操作會(huì)立即完成,這個(gè)取消令牌也就沒(méi)有用了,會(huì)被忽略。
所以,這種情況不需要用 CancellationToken,要寫(xiě)成下面的方式:
async Task SomethingAsync(CancellationToken cancellationToken)
{
var test = await Task.Run(( cancellationToken ) =>
{
...
});
...
}
寫(xiě)成這樣,才是正確的表達(dá),表達(dá)委托本身需要響應(yīng)令牌。
這是一個(gè)容易搞錯(cuò)的知識(shí)點(diǎn),記一下。