2
头图

相信之前就有很多用户想要一个取消协程的 API,迟迟没有添加进来,现在在 v4.7 版本中进行了添加:

具体实现见:#4247#4249

新增 API & 常量

新增了两个 API,分别为

Co::cancel($cid): bool
用于取消某个协程,但不能对当前协程发起取消操作

Co::isCanceled(): bool
用于判断当前协程是不是被取消的

新增了三个错误码:

常量含义
SWOOLE_ERROR_CO_CANNOT_CANCEL协程不能取消
SWOOLE_ERROR_CO_NOT_EXISTS协程不存在
SWOOLE_ERROR_CO_CANCELED协程已被取消

说明

该 API 用于从一个协程或者事件回调中取消另外一个协程。

只有处于可取消操作中的协程才能被取消, 当成功取消一个协程时, 上下文环境将会立即切换到对应协程中

尝试取消一个处于不可取消操作中的协程, Co::cancel()成功时返回 true,失败将会返回false,

此时调用swoole_last_error(),可能有两种情况:

  1. 协程不存在 SWOOLE_ERROR_CO_NOT_EXISTS
  2. 协程处于不可取消的状态 SWOOLE_ERROR_CO_CANNOT_CANCEL

可以通过Co::isCanceled()来判断当前操作是否是被手动取消的, 手动取消正常结束, 将返回true, 如失败, 将返回false

目前基本支持了绝大部分的协程 API 的取消,包括:

  1. socket
  2. AsyncIO (fread, gethostbyname ...)
  3. sleep
  4. waitSignal
  5. wait/waitpid
  6. waitEvent
  7. Co::suspend/Co::yield
  8. channel
  9. native curl (SWOOLE_HOOK_NATIVE_CURL)

有两个不可中断的场景

  1. 被 CPU 中断调度器强制切换的协程
  2. 文件锁操作期间
不过,可能在后续版本也会允许进行取消,敬请期待

使用场景

基于协程取消这一功能,可以在用户侧实现:

  • 基于协程粒度的超时熔断

在之前的版本中已挂起的协程是不可主动调度的,而Co::cancel()Co::resume()的区别就是,不止可以取消手动Co::yield()的协程,可以取消一切允许取消的协程。

  • 更好的 API 设计

和传统 PHP 的类似功能的 API 不同的是, Swoole 中大量的 API 增加了 timeout 参数, 当然也有部分难以添加或者说不合适添加 timeout 参数的, 比如文件操作系列函数, 现在一切都有了可能, 可以在 PHP 层实现任意 IO 操作的超时, 而无需依赖于底层的 API 设计

示例

下面来看一些示例代码,了解一下协程取消的用法:

不能对当前协程以及不存在的协程发起取消操作

在协程容器中自动创建了一个协程,就调用Co::cancel()进行取消,这时是不能取消的;同时协程容器中只有一个协程,去取消一个不存在的协程也是不可以的。

use Swoole\Coroutine;
use function Swoole\Coroutine\run;

run(function () {
    assert(Coroutine::cancel(Coroutine::getCid()) === false);
    assert(swoole_last_error() === SWOOLE_ERROR_CO_CANNOT_CANCEL);

    assert(Coroutine::cancel(999) === false);
    assert(swoole_last_error() === SWOOLE_ERROR_CO_NOT_EXISTS);
});

以下三个示例分别演示了在Co::suspend/Co::yieldAsyncIOchannel中使用sleep来伪造timeout后进行取消

Co::suspend/Co::yield

use Swoole\Coroutine;
use Swoole\Coroutine\System;
use function Swoole\Coroutine\run;
use function Swoole\Coroutine\go;

run(function () {
    $cid = Coroutine::getCid();
    go(function () use ($cid) {
        System::sleep(0.002);
        assert(Coroutine::cancel($cid) === true);
    });
    $retval = Coroutine::suspend();
    echo "Done\n";
    assert($retval === false);
    assert(swoole_last_error() === SWOOLE_ERROR_CO_CANCELED);
});

AsyncIO

use Swoole\Coroutine;
use Swoole\Event;
use Swoole\Coroutine\System;
use function Swoole\Coroutine\run;

run(function () {
    $cid = Coroutine::getCid();
    Event::defer(function () use ($cid) {
        assert(Coroutine::cancel($cid) === true);
    });
    $retval = System::gethostbyname('www.baidu.com');
    echo "Done\n";
    assert($retval === false);
    assert(swoole_last_error() === SWOOLE_ERROR_AIO_CANCELED);
});

channel

use Swoole\Coroutine;
use Swoole\Coroutine\System;
use function Swoole\Coroutine\run;
use function Swoole\Coroutine\go;

run(function () {
    $chan = new Coroutine\Channel(1);
    $cid = Coroutine::getCid();
    go(function () use ($cid) {
        System::sleep(0.002);
        assert(Coroutine::cancel($cid) === true);
    });

    assert($chan->push("hello world [1]", 100) === true);
    assert(Coroutine::isCanceled() === false);
    assert($chan->errCode === SWOOLE_CHANNEL_OK);

    assert($chan->push("hello world [2]", 100) === false);
    assert(Coroutine::isCanceled() === true);
    assert($chan->errCode === SWOOLE_CHANNEL_CANCELED);

    echo "Done\n";
});

当外部使用 Co::cancel() 取消一个协程的挂起状态时,该协程所调用的 API 会立即返回失败,程序代码会继续向下执行。

通过判断协程操作函数/方法返回值和错误码,或者使用 Co::isCanceled() 判断是不是被取消。


沈唁
1.9k 声望1.2k 粉丝