PHP 协程

目录
  1. 生成器作用
    1. 实例:将 range() 实现为生成器
  2. 生成器类对象
    1. 斐波那契数列
      1. 使用for循环
      2. 使用递归实现
      3. 使用yield实现
    2. 其他语法
    3. 生成器特性总结
  3. 使用协同程序实现合作多任务

生成器作用

生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大大降低。

生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样, 和普通函数只返回一次不同的是, 生成器可以根据需要 yield 多次,以便生成需要迭代的值

一个简单的例子就是使用生成器来重新实现 range() 函数。 标准的 range() 函数需要在内存中生成一个数组包含每一个在它范围内的值,然后返回该数组, 结果就是会产生多个很大的数组。 比如,调用 range(0, 1000000) 将导致内存占用超过 100 MB。

做为一种替代方法, 我们可以实现一个 xrange() 生成器, 只需要足够的内存来创建 Iterator 对象并在内部跟踪生成器的当前状态,这样只需要不到1K字节的内存。

实例:将 range() 实现为生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
function xrange($start, $limit, $step = 1) {
if ($start < $limit) {
if ($step <= 0) {
throw new LogicException('Step must be +ve');
}

for ($i = $start; $i <= $limit; $i += $step) {
yield $i;
}
} else {
if ($step >= 0) {
throw new LogicException('Step must be -ve');
}

for ($i = $start; $i >= $limit; $i += $step) {
yield $i;
}
}
}

// 注意下面range()和xrange()输出的结果是一样的。

echo 'Single digit odd numbers from range(): ';
foreach (range(1, 9, 2) as $number) {
echo "$number ";
}
echo "\n";

echo 'Single digit odd numbers from xrange(): ';
foreach (xrange(1, 9, 2) as $number) {
echo "$number ";
}

以上都会输出:

1
2
Single digit odd numbers from range():  1 3 5 7 9 
Single digit odd numbers from xrange(): 1 3 5 7 9

生成器类对象

Generator 对象是从 generators返回的.

Generator 对象不能通过 new 实例化.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Generator implements Iterator {
// 返回当前产生的值
public current ( ) : mixed
// 返回当前产生的键
public key ( ) : mixed
// 生成器继续执行
public next ( ) : void
// 重置迭代器
public rewind ( ) : void
// 向生成器中传入一个值
public send ( mixed $value ) : mixed
// 向生成器中抛入一个异常
public throw ( Exception $exception ) : void
// 检查迭代器是否被关闭
public valid ( ) : bool
// 序列化回调
public __wakeup ( ) : void
}

斐波那契数列

使用for循环

使用for循环的话,当$n数值大到一定数量,会内存溢出并程序终止。

1
2
3
4
5
6
7
8
9
function fb1($n) {
$arr = [];
$arr[0] = 1;
$arr[1] = 1;
for ($i = 2; $i <= $n; $i++) {
$arr[$i] = $arr[$i - 2] + $arr[$i - 1];
}
return $arr[$n];
}
使用递归实现
1
2
3
4
5
6
7
function FibonacciTailRecursive($n, $ret1, $ret2) {
if ($n == 1) {
return $ret1;
}
return FibonacciTailRecursive($n - 1, $ret2, $ret1 + $ret2);
}
echo FibonacciTailRecursive(500, 1, 1);

报错了,超过函数最大嵌套级别

1
Fatal error: Uncaught Error: Maximum function nesting level of '256' reached, aborting!
使用yield实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function fib($n)
{
$cur = 1;
$prev = 0;
for ($i = 0; $i < $n; $i++) {
yield $cur;

$temp = $cur;
$cur = $prev + $cur;
$prev = $temp;
}
}

$fibs = fib(500);
foreach ($fibs as $fib) {
echo " " . $fib;
}

其他语法

yield表达式也可以赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function gen()
{
while (true) {
$data = yield;
echo $data."<br>";
echo $data."<br>";
echo "test<br>";
}
echo "test2";
return "返回值";
}

$obj = gen();
var_dump($obj);

for ($i = 1; $i < 7; $i++) {
$obj->send($i);
}

生成器特性总结

  1. yield是生成器所需要的关键字,必须在函数内部,有yield的函数叫做”生成器函数”
  2. 调用生成器函数时,函数将返回一个继承了Iterator的生成器
  3. yield作为表达式使用时,可将一个值加入到生成器中进行遍历,遍历完会中断下面的语句运行,并且保存状态,当下次遍历时会继续执行(这就是while(true)没有造成阻塞的原因)
  4. 当send传入参数时,yield可作为一个变量使用,这个变量等于传入的参数

使用协同程序实现合作多任务

多任务协作这个术语中的“协作”说明了如何进行这种切换的:它要求当前正在运行的任务自动把控制传回给调度器,这样它就可以运行其他任务了。这与“抢占”多任务相反,抢占多任务是这样的:调度器可以中断运行了一段时间的任务,不管它喜欢还是不喜欢。协作多任务在Windows的早期版本(windows95)和Mac OS中有使用,不过它们后来都切换到使用抢先多任务了。理由相当明确:如果你依靠程序自动传回 控制的话,那么坏行为的软件将很容易为自身占用整个CPU,不与其他任务共享。

这个时候你应当明白协程和任务调度之间的联系:yield指令提供了任务中断自身的一种方法,然后把控制传递给调度器。因此协程可以运行多个其他任务。更进一步来说,yield可以用来在任务和调度器之间进行通信。

我们的目的是 对 “任务”用更轻量级的包装的协程函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
class Task {
protected $taskId;
protected $coroutine;
protected $sendValue = null;
protected $beforeFirstYield = true;

public function __construct($taskId, Generator $coroutine) {
$this->taskId = $taskId;
$this->coroutine = $coroutine;
}

public function getTaskId() {
return $this->taskId;
}

public function setSendValue($sendValue) {
$this->sendValue = $sendValue;
}

public function run() {
if ($this->beforeFirstYield) {
$this->beforeFirstYield = false;
return $this->coroutine->current();
} else {
$retval = $this->coroutine->send($this->sendValue);
$this->sendValue = null;
return $retval;
}
}

public function isFinished() {
return !$this->coroutine->valid();
}
}

调度器现在不得不比多任务循环要做稍微多点了,然后才运行多任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
require 'Task.php';

class Scheduler {
protected $maxTaskId = 0;
protected $taskMap = []; // taskId => task
protected $taskQueue;

public function __construct() {
$this->taskQueue = new SplQueue();
}

public function newTask(Generator $coroutine) {
$tid = ++$this->maxTaskId;
$task = new Task($tid, $coroutine);
$this->taskMap[$tid] = $task;
$this->schedule($task);
return $tid;
}

public function schedule(Task $task) {
$this->taskQueue->enqueue($task);
}

public function run() {
while (!$this->taskQueue->isEmpty()) {
$task = $this->taskQueue->dequeue();
$task->run();

if ($task->isFinished()) {
unset($this->taskMap[$task->getTaskId()]);
} else {
$this->schedule($task);
}
}
}
}

newTask()方法(使用下一个空闲的任务id)创建一个新任务,然后把这个任务放入任务映射数组里。接着它通过把任务放入任务队列里来实现对任务的调度。接着run()方法扫描任务队列,运行任务。如果一个任务结束了,那么它将从队列里删除,否则它将在队列的末尾再次被调度。

让我们看看下面具有两个简单(并且没有什么意义)任务的调度器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
require 'Scheduler.php';

function task1() {
for ($i = 1; $i <= 10; ++$i) {
echo "This is task 1 iteration $i.\n";
yield;
}
}

function task2() {
for ($i = 1; $i <= 5; ++$i) {
echo "This is task 2 iteration $i.\n";
yield;
}
}

$scheduler = new Scheduler;

$scheduler->newTask(task1());
$scheduler->newTask(task2());

$scheduler->run();

两个任务都仅仅回显一条信息,然后使用yield把控制回传给调度器。输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
This is task 1 iteration 1.
This is task 2 iteration 1.
This is task 1 iteration 2.
This is task 2 iteration 2.
This is task 1 iteration 3.
This is task 2 iteration 3.
This is task 1 iteration 4.
This is task 2 iteration 4.
This is task 1 iteration 5.
This is task 2 iteration 5.
This is task 1 iteration 6.
This is task 1 iteration 7.
This is task 1 iteration 8.
This is task 1 iteration 9.
This is task 1 iteration 10.