跳转至

取消操作

通常,我们希望一些耗时比较长且耗费资源的操作可以提供取消的方法,比如说某个数据库操作特别花时间,可以让用户直接取消执行释放资源。取消一个操作没有想象中那么简单,假如我们已经启动了一个线程执行异步的操作,那么想要取消它有以下几种方法:

  • 直接终止线程。这种方法最简单粗暴,但是也最不可用。直接终止一个线程,并不会触发栈展开以及调用析构函数。此时线程可能持有一个锁,直接终止会导致这个锁永远不会释放,或者申请了一些 RAII 资源,直接终止不会触发回收,造成资源泄露。而且,C++ 标准并没有提供终止线程的 API,需要直接调用操作系统的 API 来终止线程,降低了程序的可移植性。
  • 设置强制终止点。这种方法在取消一个线程的操作的时候,会设置一些内部标志,并在下一次的特定调用,例如 read()open() 发生的时候返回错误或者抛出异常,并进行栈展开,达到取消操作的目的。这些函数也叫取消点(Cancellation Point)。这种方式能避免资源泄露,但是这种方式有很多潜在的问题,很难实现一个完全正确的模型。同时,线程可能会在什么时候中止也并不显然。
  • 设置自愿终止点。这种方法需要函数额外接收一个取消令牌(Cancellation Token),并根据需要在不同的地方检测令牌是不是已经触发,检测到操作已经被取消的时候,就提前进行清理操作并终止函数。这种方式需要在每次调用函数的时候都传递一个取消令牌,相当于多了一个参数。正是因为这样,使用取消令牌具有传染性,会给改变大部分的函数签名。但是这种方式最为可靠、可控以及安全,能一目了然函数可能在哪里被中止,像 Go 中的 context 就相当于一个取消令牌。

一般来说,要想取消一个操作,需要先以某种方式来标识一个操作,例如可以给操作分配一个唯一的 ID,或者直接使用网络链接的文件描述符。在操作需要取消的时候,就通过标识符找到这个操作对应的线程或者取消令牌并发送取消指令。操作的取消可能有多种原因,比如用户使用另外的 API 明确地取消,或者用户直接关闭网络链接来表示需要取消这个操作。


最后更新: 2021-09-11 21:54:53
本页作者: Howard Lau