特性
Channel 通道
类似于 go 语言的 chan
,Channel
可为多生产者协程和多消费者协程模式提供支持。底层自动实现了协程的切换和调度。 Channel
与 PHP 的数组类似,仅占用内存,没有其他额外的资源申请,所有操作均为内存操作,无 I/O
消耗,使用方法与 SplQueue
队列类似。
Channel
主要用于协程间通讯,当我们希望从一个协程里返回一些数据到另一个协程时,就可通过 Channel
来进行传递。
主要方法:
Channel->push
:当队列中有其他协程正在等待pop
数据时,自动按顺序唤醒一个消费者协程。当队列已满时自动yield
让出控制权,等待其他协程消费数据Channel->pop
:当队列为空时自动yield
,等待其他协程生产数据。消费数据后,队列可写入新的数据,自动按顺序唤醒一个生产者协程。
下面是一个协程间通讯的简单例子:
代码语言:javascript复制<?php
co(function () {
$channel = new SwooleCoroutineChannel();
co(function () use ($channel) {
$channel->push('data');
});
$data = $channel->pop();
});
PHP
Copy
Defer 特性
当我们希望在协程结束时运行一些代码时,可以通过 defer(callable callable) 函数或 HyperfCoroutine::defer(callable callable) 将一段函数以 栈(stack) 的形式储存起来,栈(stack) 内的函数会在当前协程结束时以 先进后出 的流程逐个执行。
WaitGroup 特性
WaitGroup
是基于 Channel
衍生出来的一个特性,如果接触过 Go
语言,我们都会知道 WaitGroup
这一特性,在 Hyperf
里,WaitGroup
的用途是使得主协程一直阻塞等待直到所有相关的子协程都已经完成了任务后再继续运行,这里说到的阻塞等待是仅对于主协程(即当前协程)来说的,并不会阻塞当前进程。
我们通过一段代码来演示该特性:
<?php
$wg = new HyperfUtilsWaitGroup();
// 计数器加二
$wg->add(2);
// 创建协程 A
co(function () use ($wg) {
// some code
// 计数器减一
$wg->done();
});
// 创建协程 B
co(function () use ($wg) {
// some code
// 计数器减一
$wg->done();
});
// 等待协程 A 和协程 B 运行完成
$wg->wait();
PHP
Copy
注意
WaitGroup
本身也需要在协程内才能使用
Parallel 特性
Parallel
特性是 Hyperf 基于 WaitGroup
特性抽象出来的一个更便捷的使用方法,我们通过一段代码来演示一下。
<?php
use HyperfUtilsExceptionParallelExecutionException;
use HyperfUtilsCoroutine;
use HyperfUtilsParallel;
$parallel = new Parallel();
$parallel->add(function () {
sleep(1);
return Coroutine::id();
});
$parallel->add(function () {
sleep(1);
return Coroutine::id();
});
try{
// $results 结果为 [1, 2]
$results = $parallel->wait();
} catch(ParallelExecutionException $e){
// $e->getResults() 获取协程中的返回值。
// $e->getThrowables() 获取协程中出现的异常。
}
PHP
Copy
注意
HyperfUtilsExceptionParallelExecutionException
异常仅在 1.1.6 版本和更新的版本下会抛出
通过上面的代码我们可以看到仅花了 1 秒就得到了两个不同的协程的 ID,在调用 add(callable $callable)
的时候 Parallel
类会为之自动创建一个协程,并加入到 WaitGroup
的调度去。
不仅如此,我们还可以通过 parallel(array $callables)
函数进行更进一步的简化上面的代码,达到同样的目的,下面为简化后的代码。
<?php
use HyperfUtilsCoroutine;
// 传递的数组参数您也可以带上 key 便于区分子协程,返回的结果也会根据 key 返回对应的结果
$result = parallel([
function () {
sleep(1);
return Coroutine::id();
},
function () {
sleep(1);
return Coroutine::id();
}
]);
PHP
Copy
注意
Parallel
本身也需要在协程内才能使用
限制 Parallel 最大同时运行的协程数
当我们添加到 Parallel 里的任务有很多时,假设都是一些请求任务,那么一瞬间发出全部请求很有可能会导致对端服务因为一瞬间接收到了大量的请求而处理不过来,有宕机的风险,所以需要对对端进行适当的保护,但我们又希望可以通过 Parallel 机制来加速这些请求的耗时,那么可以通过在实例化 Parallel 对象时传递第一个参数,来设置最大运行的协程数,比如我们希望最大设置的协程数为 5 ,也就意味着 Parallel 里最多只会有 5 个协程在运行,只有当 5 个里有协程完成结束后,后续的协程才会继续启动,直至所有协程完成任务,示例代码如下:
代码语言:javascript复制use HyperfUtilsExceptionParallelExecutionException;
use HyperfUtilsCoroutine;
use HyperfUtilsParallel;
$parallel = new Parallel(5);
for ($i = 0; $i < 20; $i ) {
$parallel->add(function () {
sleep(1);
return Coroutine::id();
});
}
try{
$results = $parallel->wait();
} catch(ParallelExecutionException $e){
// $e->getResults() 获取协程中的返回值。
// $e->getThrowables() 获取协程中出现的异常。
}
PHP
Copy
Concurrent 协程运行控制
HyperfUtilsCoroutineConcurrent
基于 SwooleCoroutineChannel
实现,用来控制一个代码块内同时运行的最大协程数量的特性。
以下样例,当同时执行 10
个子协程时,会在循环中阻塞,但只会阻塞当前协程,直到释放出一个位置后,循环继续执行下一个子协程。
<?php
use HyperfUtilsCoroutineConcurrent;
$concurrent = new Concurrent(10);
for ($i = 0; $i < 15; $i) {
$concurrent->create(function () {
// Do something...
});
}
PHP
Copy
协程上下文
由于同一个进程内协程间是内存共享的,但协程的执行/切换是非顺序的,也就意味着我们很难掌控当前的协程是哪一个(事实上可以,但通常没人这么干),所以我们需要在发生协程切换时能够同时切换对应的上下文。 在 Hyperf 里实现协程的上下文管理将非常简单,基于 HyperfUtilsContext 类的 set(string id, value)、get(string id, default = null)、has(string id)、override(string id, Closure
HyperfUtilsContext::set()
通过调用 set(string id, value) 方法储存一个值到当前协程的上下文中,如下:
代码语言:javascript复制<?php
use HyperfUtilsContext;
// 将 bar 字符串以 foo 为 key 储存到当前协程上下文中
$foo = Context::set('foo', 'bar');
// set 方法会再将 value 作为方法的返回值返回回来,所以 $foo 的值为 bar
PHP
Copy
HyperfUtilsContext::get()
通过调用 get(string id, default = null) 方法可从当前协程的上下文中取出一个以 id 为 key 储存的值,如不存在则返回 default ,如下:
代码语言:javascript复制<?php
use HyperfUtilsContext;
// 从当前协程上下文中取出 key 为 foo 的值,如不存在则返回 bar 字符串
$foo = Context::get('foo', 'bar');
PHP
Copy
HyperfUtilsContext::has()
通过调用 has(string id) 方法可判断当前协程的上下文中是否存在以 id 为 key 储存的值,如存在则返回 true,不存在则返回 false,如下:
代码语言:javascript复制<?php
use HyperfUtilsContext;
// 从当前协程上下文中判断 key 为 foo 的值是否存在
$foo = Context::has('foo');
PHP
Copy
HyperfUtilsContext::override()
当我们需要做一些复杂的上下文处理,比如先判断一个 key 是否存在,如果存在则取出 value 来再对 value 进行某些修改,然后再将 value 设置回上下文容器中,此时会有比较繁杂的判断条件,可直接通过调用 override
方法来实现这个逻辑,如下:
<?php
use PsrHttpMessageServerRequestInterface;
use HyperfUtilsContext;
// 从协程上下文取出 $request 对象并设置 key 为 foo 的 Header,然后再保存到协程上下文中
$request = Context::override(ServerRequestInterface::class, function (ServerRequestInterface $request) {
return $request->withAddedHeader('foo', 'bar');
});
PHP
Copy
Swoole Runtime Hook Level
框架在入口函数中提供了 SWOOLE_HOOK_FLAGS
常量,如果您需要修改整个项目的 Runtime Hook
等级,比如想要支持 CURL 协程
并且 Swoole 版本为 v4.5.4
之前的版本,可以修改这里的代码,如下。
<?php
! defined('SWOOLE_HOOK_FLAGS') && define('SWOOLE_HOOK_FLAGS', SWOOLE_HOOK_ALL | SWOOLE_HOOK_CURL);
PHP
Copy
!> 如果 Swoole 版本 >= v4.5.4
,不需要做任何修改。