C#中使用本地方法时CancellationToken失效的原因解析
C# 异步本地方法挂起异常原因
复现场景说明
预期两种调用DoWait的方式都应该在1秒后触发取消逻辑返回耗时,但实际运行时:
- 直接在
Main方法中声明CancellationTokenSource调用的场景表现正常,1秒左右触发取消 - 非async的本地方法内声明
CancellationTokenSource调用的场景会永久挂起 - 给本地方法加上
async/await关键字后,运行表现恢复正常
问题复现代码如下:
using System.Diagnostics; namespace ConsoleApp2 { internal class Program { static async Task Main(string[] args) { // 不使用本地方法的正常场景 using var cts1 = new CancellationTokenSource(TimeSpan.FromSeconds(1)); Console.WriteLine(await DoWait(cts1.Token)); // 会挂起的本地方法场景 Task<long> LocalMethod() { using var cts1 = new CancellationTokenSource(TimeSpan.FromSeconds(1)); return DoWait(cts1.Token); } Console.WriteLine(await LocalMethod()); } static async Task<long> DoWait(CancellationToken ct) { var stopwatch = Stopwatch.StartNew(); var elapsedMilliseconds = stopwatch.ElapsedMilliseconds; try { while (true) { elapsedMilliseconds = stopwatch.ElapsedMilliseconds; ct.ThrowIfCancellationRequested(); await Task.Delay(10); } } catch (OperationCanceledException) { } return elapsedMilliseconds; } } }
核心原因
这个问题和CancellationToken是值类型、按值传递没有关系,本质是**CancellationTokenSource的生命周期被提前释放**,和异步方法的执行时序有关:
- 正常场景下,
Main方法里声明的cts1是async方法内的局部变量,被编译器生成的异步状态机捕获,生命周期会持续到await DoWait(cts1.Token)执行完成后才会触发using对应的Dispose逻辑。1秒超时触发时CTS还处于存活状态,取消信号可以正常传递给传入DoWait的Token,因此逻辑正常。 - 无async/await修饰的本地方法是同步执行的:
- 调用
LocalMethod()时会先同步跑完方法内全部逻辑:创建1秒超时的CTS,取出Token传入DoWait。DoWait是async方法,执行到第一个await Task.Delay(10)时就会将未完成的Task返回给调用方,此时LocalMethod的同步执行流程已经全部结束。 - LocalMethod内用
using声明的cts1会在方法同步执行结束的瞬间触发Dispose,此时距离CTS创建仅过了几毫秒,远未到1秒的超时时间。 - CTS被Dispose后,内部关联的超时定时器、取消回调注册链路都会被直接释放,永远不会触发1秒后的取消信号,DoWait持有的Token永远收不到取消通知,就会无限循环执行Delay,表现为程序挂起。
- 调用
给本地方法加上async/await后,本地方法也会被编译器编译为异步状态机,方法内的
cts1会被状态机捕获,生命周期持续到await DoWait(...)执行完成后才会Dispose,和正常场景的生命周期完全一致,CTS可以存活到超时触发的时间点,取消信号正常传递,逻辑就恢复符合预期。
误区澄清:CancellationToken虽然是值类型,传参时会发生拷贝,但拷贝出的Token内部持有指向原CTS状态的引用,只要原CTS没有提前释放,拷贝的Token完全可以正常收到取消信号,并不是值类型传参导致的信号丢失。
内容的提问来源于stack exchange,提问作者Serban Murariu
相关产品推荐
相关产品推荐

