在上一篇关于 PHP的迭代器、生成器和协程 的文章中,我们知道了,在PHP中可以通过使用yield关键字,把迭代器变成了生成器,然后通过它的让出/恢复机制,使用协程机制,在用户态进行调度。下面,我们用PHP协程机制来做一个多任务调度的示例。
1,任务执行Task
Task就是一个任务的抽象,协程就是用户空间线程,线程可以理解就是跑一个函数。
所以Task的构造函数中就是接收一个闭包函数,我们命名为coroutine。
/**
* Task任务类
*/
class Task
{
protected $taskId;
protected $coroutine;
protected $beforeFirstYield = true; //是否在第一次yield之前
protected $sendValue;
/**
* Task constructor.
* @param $taskId
* @param Generator $coroutine
*/
public function __construct($taskId, Generator $coroutine)
{
$this->taskId = $taskId;
$this->coroutine = $coroutine;
}
/**
* 获取当前的Task的ID
*
* @return mixed
*/
public function getTaskId()
{
return $this->taskId;
}
/**
* 判断Task执行完毕了没有
*
* @return bool
*/
public function isFinished()
{
return !$this->coroutine->valid();
}
/**
* 设置下次要传给协程的值,比如 $id = (yield $xxxx),这个值就给了$id了
*
* @param $value
*/
public function setSendValue($value)
{
$this->sendValue = $value;
}
/**
* 运行任务
*
* @return mixed
*/
public function run()
{
// 这里要注意,生成器的开始会reset,所以第一个值要用current获取
if ($this->beforeFirstYield) {
$this->beforeFirstYield = false;
return $this->coroutine->current();
} else {
// 用send向生成器传参
$retval = $this->coroutine->send($this->sendValue);
$this->sendValue = null;
return $retval;
}
}
}
2,任务调度Scheduler
接下来就是Scheduler这个重点核心部分,它扮演着调度员的角色。
/**
* Class Scheduler
*/
Class Scheduler
{
/**
* @var SplQueue
*/
protected $taskQueue;
/**
* @var int
*/
protected $tid = 0;
/**
* Scheduler constructor.
*/
public function __construct()
{
/* 原理就是维护了一个队列,
* 前面说过,从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制
*/
$this->taskQueue = new SplQueue();
}
/**
* 增加一个任务
*
* @param Generator $task
* @return int
*/
public function addTask(Generator $task)
{
$tid = $this->tid;
$task = new Task($tid, $task);
$this->schedule($task);
$this->tid++;
return $tid;
}
/**
* 把任务进入队列
*
* @param Task $task
*/
public function schedule(Task $task)
{
$this->taskQueue->enqueue($task);
}
/**
* 运行调度器
*/
public function run()
{
while (!$this->taskQueue->isEmpty()) {
// 任务出队
$task = $this->taskQueue->dequeue();
$res = $task->run(); // 运行任务直到 yield
if (!$task->isFinished()) {
$this->schedule($task); // 任务如果还没完全执行完毕,入队等下次执行
}
}
}
}
这样我们基本就实现了一个协程调度器。
你可以使用下面的代码来测试:
function task1() {
for ($i = 1; $i <= 10; ++$i) {
echo "This is task 1 iteration $i.
";
yield; // 主动让出CPU的执行权
}
}
function task2() {
for ($i = 1; $i <= 5; ++$i) {
echo "This is task 2 iteration $i.
";
yield; // 主动让出CPU的执行权
}
}
$scheduler = new Scheduler; // 实例化一个调度器
$scheduler->addTask(task1()); // 添加不同的闭包函数作为任务
$scheduler->addTask(task2());
$scheduler->run();
那我们再进一步地来看看,哪里能用到协程。
function task1() {
/* 这是一个远程任务,需要耗时10s,可能是一个远程机器抓取分析远程网址的任务,我们只要提交最后去远程机器拿结果就行了 */
remote_task_commit();
// 上面的请求发出后,我们不要在这里等,通过yield保存数据现场,主动让出CPU的执行权给task2运行
yield;
//恢复数据现场,从远程机器拿结果,并通过yield把结果返回给调用方
yield (remote_task_receive());
...
}
function task2() {
for ($i = 1; $i <= 5; ++$i) {
echo "This is task 2 iteration $i.
";
yield; // 主动让出CPU的执行权
}
}
这样就提高了程序的执行效率。
注意,如果在函数中使用了yield,就不能把这个函数当做普通函数来进行调用,如果你在一个协程函数中嵌套另外一个协程函数,则会导致调用不成功。这很容易理解,yield 需要进行数据现场的保存和恢复,如果嵌套了的话,则保存的和恢复的就会进行嵌套,导致错误。为了避免此种情况,需要使用 协程堆栈 这种方式,而 PHP7.0 推出了 yield from 去实现了 协程堆栈。
3,PHP7中yield from关键字
echoTimes函数
function echoTimes($msg, $max) {
for ($i = 1; $i <= $max; ++$i) {
echo "$msg iteration $i
";
yield;
}
}
task1生成器
function task1()
{
yield from echoTimes("bar", 5);
}
这样,轻松调用子协程。
4,总结
至此,我们通过 PHP的关键字 yield ,生成器,send(),current(), valid() 等 实现了一套任务调度系统。并且知道,yield from 可以实现堆栈协程 / 子协程。当然,在实际工作中,我们可能不需要自己去实现这一套系统,因为 swoole 框架已经支持了协程机制,接下来,我们会去了解一下这个框架。