相信之前就有很多用户想要一个取消协程的 API,迟迟没有添加进来,现在在 v4.7 版本中进行了添加:
具体实现见:#4247 ,#4249
新增 API & 常量
新增了两个 API,分别为
代码语言:javascript复制Co::cancel($cid): bool
用于取消某个协程,但不能对当前协程发起取消操作
和
代码语言:javascript复制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()
,可能有两种情况:
- 协程不存在
SWOOLE_ERROR_CO_NOT_EXISTS
- 协程处于不可取消的状态
SWOOLE_ERROR_CO_CANNOT_CANCEL
可以通过Co::isCanceled()
来判断当前操作是否是被手动取消的, 手动取消正常结束, 将返回true
, 如失败, 将返回false
目前基本支持了绝大部分的协程 API 的取消,包括:
- socket
- AsyncIO (fread, gethostbyname ...)
- sleep
- waitSignal
- wait/waitpid
- waitEvent
- Co::suspend/Co::yield
- channel
- native curl (SWOOLE_HOOK_NATIVE_CURL)
有两个不可中断的场景
- 被 CPU 中断调度器强制切换的协程
- 文件锁操作期间
不过,可能在后续版本也会允许进行取消,敬请期待
使用场景
基于协程取消这一功能,可以在用户侧实现:
- 基于协程粒度的超时熔断
在之前的版本中已挂起的协程是不可主动调度的,而Co::cancel()
跟Co::resume()
的区别就是,不止可以取消手动Co::yield()
的协程,可以取消一切允许取消的协程。
- 更好的 API 设计
和传统 PHP 的类似功能的 API 不同的是, Swoole 中大量的 API 增加了 timeout 参数, 当然也有部分难以添加或者说不合适添加 timeout 参数的, 比如文件操作系列函数, 现在一切都有了可能, 可以在 PHP 层实现任意 IO 操作的超时, 而无需依赖于底层的 API 设计
示例
下面来看一些示例代码,了解一下协程取消的用法:
不能对当前协程以及不存在的协程发起取消操作
在协程容器中自动创建了一个协程,就调用Co::cancel()
进行取消,这时是不能取消的;同时协程容器中只有一个协程,去取消一个不存在的协程也是不可以的。
use SwooleCoroutine;
use function SwooleCoroutinerun;
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::yield
、AsyncIO
和channel
中使用sleep
来伪造timeout
后进行取消
Co::suspend/Co::yield
代码语言:javascript复制use SwooleCoroutine;
use SwooleCoroutineSystem;
use function SwooleCoroutinerun;
use function SwooleCoroutinego;
run(function () {
$cid = Coroutine::getCid();
go(function () use ($cid) {
System::sleep(0.002);
assert(Coroutine::cancel($cid) === true);
});
$retval = Coroutine::suspend();
echo "Donen";
assert($retval === false);
assert(swoole_last_error() === SWOOLE_ERROR_CO_CANCELED);
});
AsyncIO
代码语言:javascript复制use SwooleCoroutine;
use SwooleEvent;
use SwooleCoroutineSystem;
use function SwooleCoroutinerun;
run(function () {
$cid = Coroutine::getCid();
Event::defer(function () use ($cid) {
assert(Coroutine::cancel($cid) === true);
});
$retval = System::gethostbyname('www.baidu.com');
echo "Donen";
assert($retval === false);
assert(swoole_last_error() === SWOOLE_ERROR_AIO_CANCELED);
});
channel
代码语言:javascript复制use SwooleCoroutine;
use SwooleCoroutineSystem;
use function SwooleCoroutinerun;
use function SwooleCoroutinego;
run(function () {
$chan = new CoroutineChannel(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 "Donen";
});
当外部使用 Co::cancel()
取消一个协程的挂起状态时,该协程所调用的 API 会立即返回失败,程序代码会继续向下执行。
通过判断协程操作函数/方法返回值和错误码,或者使用 Co::isCanceled()
判断是不是被取消。
好文和朋友一起看~