【发布时间】:2015-10-08 07:56:44
【问题描述】:
我遇到的情况是,对 CancellationTokenSource.Cancel 的调用永远不会返回。相反,在调用Cancel 之后(在它返回之前),执行将继续执行正在取消的代码的取消代码。如果被取消的代码随后没有调用任何可等待的代码,那么最初调用Cancel 的调用者永远不会重新获得控制权。这很奇怪。我希望Cancel 简单地记录取消请求并立即独立于取消本身返回。事实上,调用Cancel 的线程最终会执行属于被取消操作的代码,并且在返回到Cancel 的调用者之前执行此操作,这看起来像是框架中的一个错误。
这是怎么回事:
-
有一段代码,我们称之为“工作代码”,它正在等待一些异步代码。为简单起见,假设这段代码正在等待 Task.Delay:
try { await Task.Delay(5000, cancellationToken); // … } catch (OperationCanceledException) { // …. }
就在“工作代码”调用Task.Delay 之前,它正在线程T1 上执行。
延续(即“等待”之后的行或 catch 内的块)稍后将在 T1 或其他线程上执行,具体取决于一系列因素。
- 还有一段代码,我们称之为“客户端代码”,它决定取消
Task.Delay。此代码调用cancellationToken.Cancel。对Cancel的调用是在线程 T2 上进行的。
我希望线程 T2 通过返回到 Cancel 的调用者来继续。我还希望看到catch (OperationCanceledException) 的内容很快会在线程 T1 或 T2 以外的某个线程上执行。
接下来发生的事情令人惊讶。我看到在线程 T2 上,在调用 Cancel 之后,执行立即继续使用 catch (OperationCanceledException) 中的块。当Cancel 仍在调用堆栈上时,就会发生这种情况。就好像对Cancel 的调用被取消它的代码劫持了。这是显示此调用堆栈的 Visual Studio 屏幕截图:
更多上下文
以下是有关实际代码作用的更多上下文:
有一个累积请求的“工人代码”。一些“客户端代码”正在提交请求。每隔几秒钟,“工作代码”就会处理这些请求。已处理的请求将从队列中删除。
然而,偶尔,“客户端代码”会决定它达到了它希望立即处理请求的地步。为了将其传达给“工作人员代码”,它调用了“工作人员代码”提供的方法Jolt。由“客户端代码”调用的方法Jolt通过取消由工作人员的代码主循环执行的Task.Delay来实现此功能。 Worker 的代码已取消其 Task.Delay 并继续处理已排队的请求。
实际代码被精简为最简单的形式,代码为available on GitHub。
环境
该问题可以在控制台应用程序、适用于 Windows 的通用应用程序的后台代理和适用于 Windows Phone 8.1 的通用应用程序的后台代理中重现。
该问题无法在适用于 Windows 的通用应用程序中重现,其中代码按我预期的方式工作并且对 Cancel 的调用立即返回。
【问题讨论】:
-
无法在通用应用程序中重现该问题 - 因为在这种情况下,您调用
await Task.Delay(...)的线程上有一个同步上下文,因此由CancellationTokenSource.Cancel触发的延续被异步发布到该上下文。因此,没有死锁。
标签: c# asynchronous async-await task cancellationtokensource