Press "Enter" to skip to content

在PHP中使用协程实现多任务调度

PHP5.5一个比较好的新功能是加入了对迭代生成器和协程的支持.对于生成器,PHP的文档和各种其他的博客文章已经有了非常详细的讲解.协程相对受到的关注就少了,因为协程虽然有很强大的功能但相对比较复杂, 也比较难被理解,解释起来也比较困难.
这篇文章将尝试通过介绍如何使用协程来实施任务调度, 来解释在PHP中的协程.
我将在前三节做一个简单的背景介绍.如果你已经有了比较好的基础,可以直接跳到“协同多任务处理”一节.

迭代生成器

(迭代)生成器也是一个函数,不同的是这个函数的返回值是依次返回,而不是只返回一个单独的值.或者,换句话说,生成器使你能更方便的实现了迭代器接口.下面通过实现一个xrange函数来简单说明:

<?php
function xrange($start, $end, $step = 1) {
    for ($i = $start; $i <= $end; $i += $step) {
        yield $i;
    }
}
foreach (xrange(1, 1000000) as $num) {
    echo $num, "\n";
}

上面这个xrange()函数提供了和PHP的内建函数range()一样的功能.但是不同的是range()函数返回的是一个包含值从1到100万0的数组(注:请查看手册). 而xrange()函数返回的是依次输出这些值的一个迭代器, 而不会真正以数组形式返回.
这种方法的优点是显而易见的.它可以让你在处理大数据集合的时候不用一次性的加载到内存中.甚至你可以处理无限大的数据流.
当然,也可以不同通过生成器来实现这个功能,而是可以通过继承Iterator接口实现.但通过使用生成器实现起来会更方便,不用再去实现iterator接口中的5个方法了.

生成器为可中断的函数

要从生成器认识协程, 理解它内部是如何工作是非常重要的: 生成器是一种可中断的函数, 在它里面的yield构成了中断点.
还是看上面的例子, 调用xrange(1,1000000)的时候, xrange()函数里代码其实并没有真正地运行. 它只是返回了一个迭代器:

<?php
$range = xrange(1, 1000000);
var_dump($range); // object(Generator)#1
var_dump($range instanceof Iterator); // bool(true)
?>

这也解释了为什么xrange叫做迭代生成器, 因为它返回一个迭代器, 而这个迭代器实现了Iterator接口.
调用迭代器的方法一次, 其中的代码运行一次.例如, 如果你调用$range->rewind(), 那么xrange()里的代码就会运行到控制流第一次出现yield的地方. 而函数内传递给yield语句的返回值可以通过$range->current()获取.
为了继续执行生成器中yield后的代码, 你就需要调用$range->next()方法. 这将再次启动生成器, 直到下一次yield语句出现. 因此,连续调用next()和current()方法, 你就能从生成器里获得所有的值, 直到再没有yield语句出现.
对xrange()来说, 这种情形出现在$i超过$end时. 在这中情况下, 控制流将到达函数的终点,因此将不执行任何代码.一旦这种情况发生,vaild()方法将返回假, 这时迭代结束.

协程

协程的支持是在迭代生成器的基础上, 增加了可以回送数据给生成器的功能(调用者发送数据给被调用的生成器函数). 这就把生成器到调用者的单向通信转变为两者之间的双向通信.
传递数据的功能是通过迭代器的send()方法实现的. 下面的logger()协程是这种通信如何运行的例子:

<?php
function logger($fileName) {
    $fileHandle = fopen($fileName, 'a');
    while (true) {
        fwrite($fileHandle, yield . "\n");
    }
}
$logger = logger(__DIR__ . '/log');
$logger->send('Foo');
$logger->send('Bar')
?>

正如你能看到,这儿yield没有作为一个语句来使用, 而是用作一个表达式, 即它能被演化成一个值. 这个值就是调用者传递给send()方法的值. 在这个例子里, yield表达式将首先被"Foo"替代写入Log, 然后被"Bar"替代写入Log.
上面的例子里演示了yield作为接受者, 接下来我们看如何同时进行接收和发送的例子:

<?php
function gen() {
    $ret = (yield 'yield1');
    var_dump($ret);
    $ret = (yield 'yield2');
    var_dump($ret);
}
$gen = gen();
var_dump($gen->current());    // string(6) "yield1"
var_dump($gen->send('ret1')); // string(4) "ret1"   (the first var_dump in gen)
                              // string(6) "yield2" (the var_dump of the ->send() return value)
var_dump($gen->send('ret2')); // string(4) "ret2"   (again from within gen)
                              // NULL               (the return value of ->send())
?>

要很快的理解输出的精确顺序可能稍微有点困难, 但你确定要搞清楚为什按照这种方式输出. 以便后续继续阅读.
另外, 我要特别指出的有两点:
第一点,yield表达式两边的括号在PHP7以前不是可选的, 也就是说在PHP5.5和PHP5.6中圆括号是必须的.
第二点,你可能已经注意到调用current()之前没有调用rewind().这是因为生成迭代对象的时候已经隐含地执行了rewind操作.

多任务协作

如果阅读了上面的logger()例子, 你也许会疑惑“为了双向通信我为什么要使用协程呢?我完全可以使用其他非协程方法实现同样的功能啊?", 是的, 你是对的, 但上面的例子只是为了演示了基本用法, 这个例子其实并没有真正的展示出使用协程的优点.
正如上面介绍里提到的,协程是非常强大的概念,不过却应用的很稀少而且常常十分复杂.要给出一些简单而真实的例子很难.
在这篇文章里,我决定去做的是使用协程实现多任务协作.我们要解决的问题是你想并发地运行多任务(或者“程序”).不过我们都知道CPU在一个时刻只能运行一个任务(不考虑多核的情况).因此处理器需要在不同的任务之间进行切换,而且总是让每个任务运行 “一小会儿”.
多任务协作这个术语中的“协作”很好的说明了如何进行这种切换的:它要求当前正在运行的任务自动把控制传回给调度器,这样就可以运行其他任务了. 这与“抢占”多任务相反, 抢占多任务是这样的:调度器可以中断运行了一段时间的任务, 不管它喜欢还是不喜欢. 协作多任务在Windows的早期版本(windows95)和Mac OS中有使用, 不过它们后来都切换到使用抢先多任务了. 理由相当明确:如果你依靠程序自动交出控制的话, 那么一些恶意的程序将很容易占用整个CPU, 不与其他任务共享.
现在你应当明白协程和任务调度之间的关系:yield指令提供了任务中断自身的一种方法, 然后把控制交回给任务调度器. 因此协程可以运行多个其他任务. 更进一步来说, yield还可以用来在任务和调度器之间进行通信.
为了实现我们的多任务调度, 首先实现“任务” -- 一个用轻量级的包装的协程函数:

<?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();
    }
}

如代码, 一个任务就是用任务ID标记的一个协程(函数). 使用setSendValue()方法, 你可以指定哪些值将被发送到下次的恢复(在之后你会了解到我们需要这个), run()函数确实没有做什么, 除了调用send()方法的协同程序, 要理解为什么添加了一个 beforeFirstYieldflag变量, 需要考虑下面的代码片段:

<?php
function gen() {
    yield 'foo';
    yield 'bar';
}
$gen = gen();
var_dump($gen->send('something'));
// 如之前提到的在send之前, 当$gen迭代器被创建的时候一个renwind()方法已经被隐式调用
// 所以实际上发生的应该类似:
//$gen->rewind();
//var_dump($gen->send('something'));
//这样renwind的执行将会导致第一个yield被执行, 并且忽略了他的返回值.
//真正当我们调用yield的时候, 我们得到的是第二个yield的值! 导致第一个yield的值被忽略.
//string(3) "bar"

通过添加 beforeFirstYieldcondition 我们可以确定第一个yield的值能被正确返回.
调度器现在不得不比多任务循环要做稍微多点了, 然后才运行多任务:

<?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)创建一个新任务,然后把这个任务放入任务map数组里. 接着它通过把任务放入任务队列里来实现对任务的调度. 接着run()方法扫描任务队列, 运行任务.如果一个任务结束了, 那么它将从队列里删除, 否则它将在队列的末尾再次被调度.
让我们看看下面具有两个简单(没有什么意义)任务的调度器:

<?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把控制回传给调度器.输出结果如下:

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.

输出确实如我们所期望的:对前五个迭代来说,两个任务是交替运行的, 而在第二个任务结束后, 只有第一个任务继续运行.

与调度器之间通信

既然调度器已经运行了, 那么我们来看下一个问题:任务和调度器之间的通信.
我们将使用进程用来和操作系统会话的同样的方式来通信:系统调用.
我们需要系统调用的理由是操作系统与进程相比它处在不同的权限级别上. 因此为了执行特权级别的操作(如杀死另一个进程), 就不得不以某种方式把控制传回给内核, 这样内核就可以执行所说的操作了. 再说一遍, 这种行为在内部是通过使用中断指令来实现的. 过去使用的是通用的int指令, 如今使用的是更特殊并且更快速的syscall/sysenter指令.
我们的任务调度系统将反映这种设计:不是简单地把调度器传递给任务(这样就允许它做它想做的任何事), 我们将通过给yield表达式传递信息来与系统调用通信. 这儿yield即是中断, 也是传递信息给调度器(和从调度器传递出信息)的方法.
为了说明系统调用, 我们对可调用的系统调用做一个小小的封装:

<?php
class SystemCall {
    protected $callback;
    public function __construct(callable $callback) {
        $this->callback = $callback;
    }
    public function __invoke(Task $task, Scheduler $scheduler) {
        $callback = $this->callback;
        return $callback($task, $scheduler);
    }
}

它和其他任何可调用的对象(使用_invoke)一样的运行, 不过它要求调度器把正在调用的任务和自身传递给这个函数.
为了解决这个问题我们不得不微微的修改调度器的run方法:

<?php
public function run() {
    while (!$this->taskQueue->isEmpty()) {
        $task = $this->taskQueue->dequeue();
        $retval = $task->run();
        if ($retval instanceof SystemCall) {
            $retval($task, $this);
            continue;
        }
        if ($task->isFinished()) {
            unset($this->taskMap[$task->getTaskId()]);
        } else {
            $this->schedule($task);
        }
    }
}

第一个系统调用除了返回任务ID外什么都没有做:

<?php
function getTaskId() {
    return new SystemCall(function(Task $task, Scheduler $scheduler) {
        $task->setSendValue($task->getTaskId());
        $scheduler->schedule($task);
    });
}

这个函数设置任务id为下一次发送的值, 并再次调度了这个任务 .由于使用了系统调用, 所以调度器不能自动调用任务, 我们需要手工调度任务(稍后你将明白为什么这么做). 要使用这个新的系统调用的话, 我们要重新编写以前的例子:

<?php
function task($max) {
    $tid = (yield getTaskId()); // <-- here's the syscall!
    for ($i = 1; $i <= $max; ++$i) {
        echo "This is task $tid iteration $i.\n";
        yield;
    }
}
$scheduler = new Scheduler;
$scheduler->newTask(task(10));
$scheduler->newTask(task(5));
$scheduler->run();
?>

这段代码将给出与前一个例子相同的输出. 请注意系统调用如何同其他任何调用一样正常地运行, 只不过预先增加了yield.
要创建新的任务, 然后再杀死它们的话, 需要两个以上的系统调用:

<?php
function newTask(Generator $coroutine) {
    return new SystemCall(
        function(Task $task, Scheduler $scheduler) use ($coroutine) {
            $task->setSendValue($scheduler->newTask($coroutine));
            $scheduler->schedule($task);
        }
    );
}
function killTask($tid) {
    return new SystemCall(
        function(Task $task, Scheduler $scheduler) use ($tid) {
            $task->setSendValue($scheduler->killTask($tid));
            $scheduler->schedule($task);
        }
    );
}

killTask函数需要在调度器里增加一个方法:

<?php
public function killTask($tid) {
    if (!isset($this->taskMap[$tid])) {
        return false;
    }
    unset($this->taskMap[$tid]);
    // This is a bit ugly and could be optimized so it does not have to walk the queue,
    // but assuming that killing tasks is rather rare I won't bother with it now
    foreach ($this->taskQueue as $i => $task) {
        if ($task->getTaskId() === $tid) {
            unset($this->taskQueue[$i]);
            break;
        }
    }
    return true;
}

用来测试新功能的微脚本:

<?php
function childTask() {
    $tid = (yield getTaskId());
    while (true) {
        echo "Child task $tid still alive!\n";
        yield;
    }
}
function task() {
    $tid = (yield getTaskId());
    $childTid = (yield newTask(childTask()));
    for ($i = 1; $i <= 6; ++$i) {
        echo "Parent task $tid iteration $i.\n";
        yield;
        if ($i == 3) yield killTask($childTid);
    }
}
$scheduler = new Scheduler;
$scheduler->newTask(task());
$scheduler->run();
?>

这段代码将打印以下信息:

Parent task 1 iteration 1.
Child task 2 still alive!
Parent task 1 iteration 2.
Child task 2 still alive!
Parent task 1 iteration 3.
Child task 2 still alive!
Parent task 1 iteration 4.
Parent task 1 iteration 5.
Parent task 1 iteration 6.

经过三次迭代以后子任务将被杀死, 因此这就是"Child is still alive"消息结束的时候. 不过你要明白这还不是真正的父子关系. 因为在父任务结束后子任务仍然可以运行, 子任务甚至可以杀死父任务. 可以修改调度器使它具有更层级化的任务结构, 不过这个不是我们这个文章要继续讨论的范围了.
现在你可以实现许多进程管理调用. 例如 wait(它一直等待到任务结束运行时), exec(它替代当前任务)和fork(它创建一个当前任务的克隆). fork非常酷,而 且你可以使用PHP的协程真正地实现它, 因为它们都支持克隆.
让我们把这些留给有兴趣的读者吧,我们来看下一个议题.

非阻塞IO

很明显, 我们的任务管理系统的真正很酷的应用应该是web服务器. 它有一个任务是在套接字上侦听是否有新连接, 当有新连接要建立的时候, 它创建一个新任务来处理新连接.
Web服务器最难的部分通常是像读数据这样的套接字操作是阻塞的. 例如PHP将等待到客户端完成发送为止. 对一个Web服务器来说, 这有点不太高效. 因为服务器在一个时间点上只能处理一个连接.
解决方案是确保在真正对套接字读写之前该套接字已经“准备就绪”. 为了查找哪个套接字已经准备好读或者写了, 可以使用 流选择函数.
首先,让我们添加两个新的 syscall, 它们将等待直到指定socket 准备好:

<?php
function waitForRead($socket) {
    return new SystemCall(
        function(Task $task, Scheduler $scheduler) use ($socket) {
            $scheduler->waitForRead($socket, $task);
        }
    );
}
function waitForWrite($socket) {
    return new SystemCall(
        function(Task $task, Scheduler $scheduler) use ($socket) {
            $scheduler->waitForWrite($socket, $task);
        }
    );
}

这些 syscall 只是在调度器中代理其各自的方法:

<?php
// resourceID => [socket, tasks]
protected $waitingForRead = [];
protected $waitingForWrite = [];
public function waitForRead($socket, Task $task) {
    if (isset($this->waitingForRead[(int) $socket])) {
        $this->waitingForRead[(int) $socket][1][] = $task;
    } else {
        $this->waitingForRead[(int) $socket] = [$socket, [$task]];
    }
}
public function waitForWrite($socket, Task $task) {
    if (isset($this->waitingForWrite[(int) $socket])) {
        $this->waitingForWrite[(int) $socket][1][] = $task;
    } else {
        $this->waitingForWrite[(int) $socket] = [$socket, [$task]];
    }
}

waitingForRead 及 waitingForWrite 属性是两个承载等待的socket 及等待它们的任务的数组. 有趣的部分在于下面的方法,它将检查 socket 是否可用, 并重新安排各自任务:

<?php
protected function ioPoll($timeout) {
    $rSocks = [];
    foreach ($this->waitingForRead as list($socket)) {
        $rSocks[] = $socket;
    }
    $wSocks = [];
    foreach ($this->waitingForWrite as list($socket)) {
        $wSocks[] = $socket;
    }
    $eSocks = []; // dummy
    if (!stream_select($rSocks, $wSocks, $eSocks, $timeout)) {
        return;
    }
    foreach ($rSocks as $socket) {
        list(, $tasks) = $this->waitingForRead[(int) $socket];
        unset($this->waitingForRead[(int) $socket]);
        foreach ($tasks as $task) {
            $this->schedule($task);
        }
    }
    foreach ($wSocks as $socket) {
        list(, $tasks) = $this->waitingForWrite[(int) $socket];
        unset($this->waitingForWrite[(int) $socket]);
        foreach ($tasks as $task) {
            $this->schedule($task);
        }
    }
}

stream_select 函数接受承载读取、写入以及待检查的socket的数组(我们无需考虑最后一类). 数组将按引用传递, 函数只会保留那些状态改变了的数组元素. 我们可以遍历这些数组, 并重新安排与之相关的任务.
为了正常地执行上面的轮询动作, 我们将在调度器里增加一个特殊的任务:

<?php
protected function ioPollTask() {
    while (true) {
        if ($this->taskQueue->isEmpty()) {
            $this->ioPoll(null);
        } else {
            $this->ioPoll(0);
        }
        yield;
    }
}
?>

需要在某个地方注册这个任务, 例如, 你可以在run()方法的开始增加$this->newTask($this->ioPollTask()). 然后就像其他任务一样每执行完整任务循环一次就执行轮询操作一次(这么做一定不是最好的方法), ioPollTask将使用0秒的超时来调用ioPoll, 也就是stream_select将立即返回(而不是等待).
只有任务队列为空时,我们才使用null超时,这意味着它一直等到某个套接口准备就绪.如果我们没有这么做,那么轮询任务将一而再, 再而三的循环运行, 直到有新的连接建立. 这将导致100%的CPU利用率. 相反, 让操作系统做这种等待会更有效.
现在编写服务器就相对容易了:

<?php
function server($port) {
    echo "Starting server at port $port...\n";
    $socket = @stream_socket_server("tcp://localhost:$port", $errNo, $errStr);
    if (!$socket) throw new Exception($errStr, $errNo);
    stream_set_blocking($socket, 0);
    while (true) {
        yield waitForRead($socket);
        $clientSocket = stream_socket_accept($socket, 0);
        yield newTask(handleClient($clientSocket));
    }
}
function handleClient($socket) {
    yield waitForRead($socket);
    $data = fread($socket, 8192);
    $msg = "Received following request:\n\n$data";
    $msgLength = strlen($msg);
    $response = <<<res
HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: $msgLength\r
Connection: close\r
\r
$msg
RES;
    yield waitForWrite($socket);
    fwrite($socket, $response);
    fclose($socket);
}
$scheduler = new Scheduler;
$scheduler->newTask(server(8000));
$scheduler->run();

这段代码实现了接收localhost:8000上的连接, 然后返回发送来的内容作为HTTP响应. 当然它还能处理真正的复杂HTTP请求, 上面的代码片段只是演示了一般性的概念.
你可以使用类似于ab -n 10000 -c 100 localhost:8000/这样命令来测试服务器. 这条命令将向服务器发送10000个请求, 并且其中100个请求将同时到达. 使用这样的数目, 我得到了处于中间的10毫秒的响应时间. 不过还有一个问题:有少数几个请求真正处理的很慢(如5秒), 这就是为什么总吞吐量只有2000请求/秒(如果是10毫秒的响应时间的话, 总的吞吐量应该更像是10000请求/秒)

协程堆栈

如果你试图用我们的调度系统建立更大的系统的话, 你将很快遇到问题:我们习惯了把代码分解为更小的函数, 然后调用它们. 然而, 如果使用了协程的话, 就不能这么做了. 例如,看下面代码:

<?php
function echoTimes($msg, $max) {
    for ($i = 1; $i <= $max; ++$i) {
        echo "$msg iteration $i\n";
        yield;
    }
}
function task() {
    echoTimes('foo', 10); // print foo ten times
    echo "---\n";
    echoTimes('bar', 5); // print bar five times
    yield; // force it to be a coroutine
}
$scheduler = new Scheduler;
$scheduler->newTask(task());
$scheduler->run();

这段代码试图把重复循环“输出n次“的代码嵌入到一个独立的协程里,然后从主任务里调用它. 然而它无法运行. 正如在这篇文章的开始所提到的, 调用生成器(或者协程)将没有真正地做任何事情, 它仅仅返回一个对象.这 也出现在上面的例子里:echoTimes调用除了放回一个(无用的)协程对象外不做任何事情.
为了仍然允许这么做,我们需要在这个裸协程上写一个小小的封装.我们将调用它:“协程堆栈”. 因为它将管理嵌套的协程调用堆栈. 这将是通过生成协程来调用子协程成为可能:

$retval = (yield someCoroutine($foo, $bar));

使用yield,子协程也能再次返回值:

yield retval("I'm a return value!");

retval函数除了返回一个值的封装外没有做任何其他事情.这个封装将表示它是一个返回值.

<?php
class CoroutineReturnValue {
    protected $value;
    public function __construct($value) {
        $this->value = $value;
    }
    public function getValue() {
        return $this->value;
    }
}
function retval($value) {
    return new CoroutineReturnValue($value);
}

为了把协程转变为协程堆栈(它支持子调用),我们将不得不编写另外一个函数(很明显,它是另一个协程):

<?php
function stackedCoroutine(Generator $gen) {
    $stack = new SplStack;
    for (;;) {
        $value = $gen->current();
        if ($value instanceof Generator) {
            $stack->push($gen);
            $gen = $value;
            continue;
        }
        $isReturnValue = $value instanceof CoroutineReturnValue;
        if (!$gen->valid() || $isReturnValue) {
            if ($stack->isEmpty()) {
                return;
            }
            $gen = $stack->pop();
            $gen->send($isReturnValue ? $value->getValue() : NULL);
            continue;
        }
        $gen->send(yield $gen->key() => $value);
    }
}

这个函数在调用者和当前正在运行的子协程之间扮演着简单代理的角色.在$gen->send(yield $gen->key()=>$value);这行完成了代理功能.另外它检查返回值是否是生成器,万一是生成器的话,它将开始运行这个生成器,并把前一个协程压入堆栈里.一旦它获得了CoroutineReturnValue的话,它将再次请求堆栈弹出,然后继续执行前一个协程.
为了使协程堆栈在任务里可用,任务构造器里的$this-coroutine =$coroutine;这行需要替代为$this->coroutine = StackedCoroutine($coroutine);.
现在我们可以稍微改进上面web服务器例子:把wait+read(和wait+write和warit+accept)这样的动作分组为函数.为了分组相关的 功能,我将使用下面类:

<?php
class CoSocket {
    protected $socket;
    public function __construct($socket) {
        $this->socket = $socket;
    }
    public function accept() {
        yield waitForRead($this->socket);
        yield retval(new CoSocket(stream_socket_accept($this->socket, 0)));
    }
    public function read($size) {
        yield waitForRead($this->socket);
        yield retval(fread($this->socket, $size));
    }
    public function write($string) {
        yield waitForWrite($this->socket);
        fwrite($this->socket, $string);
    }
    public function close() {
        @fclose($this->socket);
    }
}

现在服务器可以编写的稍微简洁点了:

<?php
function server($port) {
    echo "Starting server at port $port...\n";
    $socket = @stream_socket_server("tcp://localhost:$port", $errNo, $errStr);
    if (!$socket) throw new Exception($errStr, $errNo);
    stream_set_blocking($socket, 0);
    $socket = new CoSocket($socket);
    while (true) {
        yield newTask(
            handleClient(yield $socket->accept())
        );
    }
}
function handleClient($socket) {
    $data = (yield $socket->read(8192));
    $msg = "Received following request:\n\n$data";
    $msgLength = strlen($msg);
    $response = <<<res
HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: $msgLength\r
Connection: close\r
\r
$msg
RES;
    yield $socket->write($response);
    yield $socket->close();
}

错误处理

作为一个优秀的程序员, 相信你已经察觉到上面的例子缺少错误处理. 几乎所有的 socket 都是易出错的. 我没有这样做的原因一方面固然是因为错误处理的乏味(特别是 socket), 另一方面也在于它很容易使代码体积膨胀.
不过, 我仍然想讲下常见的协程错误处理:协程允许使用 throw() 方法在其内部抛出一个错误.
throw() 方法接受一个 Exception, 并将其抛出到协程的当前悬挂点, 看看下面代码:

<?php
function gen() {
    echo "Foo\n";
    try {
        yield;
    } catch (Exception $e) {
        echo "Exception: {$e->getMessage()}\n";
    }
    echo "Bar\n";
}
$gen = gen();
$gen->rewind();                     // echos "Foo"
$gen->throw(new Exception('Test')); // echos "Exception: Test"
                                    // and "Bar"

这非常好, 有没有? 因为我们现在可以使用系统调用以及子协程调用异常抛出了.
不过我们要对系统调用Scheduler::run() 方法做一些小调整:

<?php
if ($retval instanceof SystemCall) {
    try {
        $retval($task, $this);
    } catch (Exception $e) {
        $task->setException($e);
        $this->schedule($task);
    }
    continue;
}

Task 类也要添加 throw 调用处理:

<?php
class Task {
    // ...
    protected $exception = null;
    public function setException($exception) {
        $this->exception = $exception;
    }
    public function run() {
        if ($this->beforeFirstYield) {
            $this->beforeFirstYield = false;
            return $this->coroutine->current();
        } elseif ($this->exception) {
            $retval = $this->coroutine->throw($this->exception);
            $this->exception = null;
            return $retval;
        } else {
            $retval = $this->coroutine->send($this->sendValue);
            $this->sendValue = null;
            return $retval;
        }
    }
    // ...
}

现在, 我们已经可以在系统调用中使用异常抛出了!例如,要调用 killTask,让我们在传递 ID 不可用时抛出一个异常:

<?php
function killTask($tid) {
    return new SystemCall(
        function(Task $task, Scheduler $scheduler) use ($tid) {
            if ($scheduler->killTask($tid)) {
                $scheduler->schedule($task);
            } else {
                throw new InvalidArgumentException('Invalid task ID!');
            }
        }
    );
}

试试看:

<?php
function task() {
    try {
        yield killTask(500);
    } catch (Exception $e) {
        echo 'Tried to kill task 500 but failed: ', $e->getMessage(), "\n";
    }
}

这些代码现在尚不能正常运作,因为 stackedCoroutine 函数无法正确处理异常.要修复需要做些调整:

<?php
function stackedCoroutine(Generator $gen) {
    $stack = new SplStack;
    $exception = null;
    for (;;) {
        try {
            if ($exception) {
                $gen->throw($exception);
                $exception = null;
                continue;
            }
            $value = $gen->current();
            if ($value instanceof Generator) {
                $stack->push($gen);
                $gen = $value;
                continue;
            }
            $isReturnValue = $value instanceof CoroutineReturnValue;
            if (!$gen->valid() || $isReturnValue) {
                if ($stack->isEmpty()) {
                    return;
                }
                $gen = $stack->pop();
                $gen->send($isReturnValue ? $value->getValue() : NULL);
                continue;
            }
            try {
                $sendValue = (yield $gen->key() => $value);
            } catch (Exception $e) {
                $gen->throw($e);
                continue;
            }
            $gen->send($sendValue);
        } catch (Exception $e) {
            if ($stack->isEmpty()) {
                throw $e;
            }
            $gen = $stack->pop();
            $exception = $e;
        }
    }
}

结束语

在这篇文章里,我使用多任务协作构建了一个任务调度器, 其中包括执行“系统调用”, 做非阻塞操作和处理错误. 所有这些里真正很酷的事情是任务的结果代码看起来完全同步, 甚至任务正在执行大量的异步操作的时候也是这样.
如果你打算从套接口读取数据的话, 你将不需要传递某个回调函数或者注册一个事件侦听器. 相反, 你只要书写yield $socket->read(). 这儿大部分都是你常常也要编写的,只 在它的前面增加yield.
当我第一次听到协程的时候, 我发现这个概念完全令人折服, 正是因为这个激励我在PHP中实现了它. 同时我发现协程真正非常的令人惊叹:在令人敬畏的代码和一大堆乱代码之间只有一线之隔, 我认为协程恰好处在这条线上, 不多不少. 不过, 要说使用上面所述的方法书写异步代码是否真的有益, 这个就见仁见智了.
但, 不管咋样, 我认为这是一个有趣的话题, 而且我希望你也能找到它的乐趣. 欢迎评论:)

141 Comments

  1. Godrej Ananda
    Godrej Ananda July 8, 2023

    Godrej Ananda firmly believes that the key to success lies in one essential aspect. By upholding unwavering values, prioritizing customers, ensuring robust engineering, and maintaining transparent business practices

  2. Mason
    Mason May 28, 2023

    看到非阻塞IO后面直接懵逼了,勉强看完,后续会再来学习

  3. Prestige Park Grove
    Prestige Park Grove March 11, 2023

    This post was exactly what I needed to read today – thank you for sharing your perspective on this topic.

  4. sexy baccarat
    sexy baccarat August 4, 2022

    you are so cool to me I really love your work. and I will continue to follow you.

  5. muhit
    muhit July 22, 2022

    thanks a lot for sharing

  6. Tata Carnatica
    Tata Carnatica July 5, 2022

    Tata Carnatica offers 2 BHK Apartments of many sizes. The magnitude of area included in this property vary depending on the number of BHKs. Tata Carnatica is spread over a wide area with 5 floors. The master plan of Tata Carnatica is designed in such a way that these Apartments comprise of wide space with proper ventilation at every corner of the house. The interiors are beautifully crafted with a designer tiled floor, granite counter slab in the kitchen, modern sanitary fittings in the bathroom, and huge windows for proper sunlight.

  7. Godrej Splendour
    Godrej Splendour June 25, 2022

    The article you have shared here very good. This is really interesting information for me. Thanks for sharing!

  8. Jakop
    Jakop June 18, 2022

    nice article

  9. AISYAH
    AISYAH June 18, 2022

    good article…..

  10. tangtang
    tangtang November 18, 2021

    真的是难啊!看的一脸懵逼,有没有什么简单易懂的东西

  11. andyliang
    andyliang September 30, 2021

    getTaskId 是不是应该叫 getSystemCall 哇

  12. franky
    franky July 13, 2021

    第七次看这篇文章,socket这里又停下来没看下去

  13. Aaron
    Aaron March 26, 2021

    socket有stream_select这个函数可以检查是否可用。
    如果是操作mysql的话,可以用这种协程的办法实现异步吗?

  14. tuzkii
    tuzkii February 24, 2021

    我们将调用它:“协程堆栈” 应译为 我们叫它: “协程堆栈”

  15. janmas
    janmas October 8, 2020

    first blood!!基本没太看懂。。我太菜了

  16. Heropoo
    Heropoo April 27, 2020

    今天又来看了

  17. sblack
    sblack April 8, 2020

    还真是难理解,我还是没看懂$task_id = (yield getTaskId());这个怎么得到$task_id的

    • Klusfq
      Klusfq June 29, 2020

      1、getTaskId()函数,返回的是一个以「匿名函数」为构造函数参数的SystemCall实例;它作为此生成器被调度的生成值——调度器获得。
      2、调度器会判断任务调度结果是否instanceof SystemCall;如果是,则以当前上下文(即scheduler->run()中)的$scheduler和$task作为参数,执行这个SystemCall实例(它实现了__invoke(),所以可执行),而__invoke就做了一件事——调用「匿名函数」。

    • stone
      stone July 30, 2020

      $tid = (yield getTaskId());是通过getTaskId()方法里的$task->setSendValue($task->getTaskId());发送的,通过yield接收

  18. Klusfq
    Klusfq March 22, 2020

    三刷打卡~~~

    看第一遍的时候,80%是懵逼的;
    看第二遍的时候,懵懵懂懂觉得自己应该懂了;
    三个月过去了,第三刷打卡!!!

    • Klusfq
      Klusfq June 29, 2020

      四刷打卡~~~
      才参透了非阻塞IO之前的东西了……

      唉,我都开始怀疑自己智商了。

  19. […] 一开始我觉得这是不可能的,直到我看到了这片文章:Cooperative multitasking using coroutines (in PHP!)。当然,我看的是中文版: 在PHP中使用协程实现多任务调度,文中提到了PHP5.5加入的一个新功能:yield。 […]

  20. temple run
    temple run September 30, 2019

    Your article is really interesting and meaningful, I have read many articles but none of them convinced me, thank you for sharing.

  21. Dovino
    Dovino July 6, 2019

    非常有帮助,能够学习更好,更有用,非常感谢您的分享

  22. thors
    thors June 27, 2019

    发现一个有趣的地方
    function test(){
    exit;
    yield 123;
    }
    test();
    echo ‘输出即代表没有执行exit’;
    如果函数内有yield语句,在yield之前的代码只有在遍历的时候,才会执行

    • Alan
      Alan July 9, 2019

      yield 返回的是一个 Generator 对象,是一个可遍历的对象,也就是说,你直接调用 test() 函数是不行的,你可以调用 test() -> current() 获取 ,也可以直接foreach遍历 test()来迭代这个对象。

  23. Heropoo
    Heropoo June 10, 2019

    先学习,慢慢消化

  24. ruibinzhang
    ruibinzhang April 12, 2019

    做多任务调度,poll方式效率似乎很低,跟网络select poll方式一样,需要轮询;
    yield在需要返回大量数据特别占内存的场景下非常有用,这可以用于生产者
    、消费者场景里,生产者产生大量的数据,消费者一条条往后面搬运,
    非常适合常驻进程,多进程调度的场景,但是又如上面的例子使用poll轮询的方式来实现异步IO效率比较低,如何让yield实现高效的epoll网络处理模型?

  25. LGD
    LGD April 12, 2019

    基本看不懂,大神能不能通俗化点啊

  26. […] 其实在写本文之前,我对生成器以及基于此特性延伸出来的 php 的协程实现并没有比较直观的了解,主要是我个人水平并不是很高,属于典型的刚入了门的 PHPer。所以在看了前段时间鸟哥(laruence)博客中对协程的讲解(原文链接:《在PHP中使用协程实现多任务调度》)后,在我个人对本篇的理解上,针对那些比较难以理解的概念(包括我个人在理解这一概念的时候的难点),以一个更为通俗的方式去讲明白。当然由于本人也是刚刚去学习这一概念,所以有些不得当的地方在所难免,希望大神看见了请不吝赐教。 […]

  27. yang
    yang March 21, 2019

    这篇文章多任务调度那部分基本看不太懂,谁能说下在看这篇文章前,需要哪些先行知识做基础吗?

  28. wenjy
    wenjy March 14, 2019

    最后一个server启动不成功,没有报错也没看到tcp 8000端口,环境 mac php7.0、php5.6,

  29. […] yield 最简单的调用形式看起来像一个 return 申明,不同的是 yield 暂停当前过程的执行并返回值,而 return 是中断当前过程并返回值。暂停当前过程,意味着将处理权转交由上一级继续进行,直到上一级再次调用被暂停的过程,该过程又会从上一次暂停的位置继续执行。这像是什么呢?如果之前已经在鸟哥的文章中粗略看过,应该知道这很像操作系统的进程调度,多个进程在一个 CPU 核心上执行,在系统调度下每一个进程执行一段指令就被暂停,切换到下一个进程,这样外部用户看起来就像是同时在执行多个任务。 […]

  30. […]   上面粗略地介绍一下yield实现协程的原理,基于这种阻塞以及接收运行的模式,我们可以轻松地实现用户态的多任务协作。   知道了协程存在的意义以及具体的实现,遗憾的是Java并不支持协程,具体原因自己并没有去深究,好在nikic大神给php添加了这个特性,如果要深入理解php多任务协程调度,可以参考nikic大神的文章,里面用yield hack了许多酷酷的功能,比如协程版本的非阻塞io,利用协程堆栈实现生成器调用子协程,有兴趣可以了解一下: Cooperative multitasking using coroutines (in PHP!)   如果英语不太行,鸟哥(laruence)对这篇文章进行了翻译,这里给出中文版的链接: 在PHP中使用协程实现多任务调度 […]

  31. aaron
    aaron January 21, 2019

    我建议看完感觉看得浑浑噩噩的朋友可以先放一下,过几天或几个星期再来看一遍,到时候因为脑中已经有模糊的知识图像,看起来就能很好理解了(至少我是这种情况)。

  32. henry
    henry January 2, 2019

    我是学了nodejs才看懂的,gen就是generator,nodejs现在直接async和await替代了

  33. php二次开发
    php二次开发 December 26, 2018

    感谢分享,虽然我还在学习当中,但是有大师们的提点我想我能学会的!!

  34. Davi
    Davi November 19, 2018

    非常感谢分享

  35. ldh
    ldh October 23, 2018

    SystemCall类中的__invoke方法自动调用时,里面的task及$scheduler是怎么传的值,看不懂啊。。。

    • ldh
      ldh October 24, 2018

      看懂了,继续学习中

  36. PHP回顾之协程 – 1
    PHP回顾之协程 – 1 October 14, 2018

    […] 说到PHP中的协程编程,相信大部分人已经看过鸟哥转载(翻译)的这篇博文:在PHP中使用协程实现多任务调度。原文作者 nikic 是PHP的核心开发者,生成器功能的倡议者和实现人。想深入了解生成器及基于其的协程编程,nikic关于生成器的RFC和鸟哥网站上的文章必读。 […]

  37. […] yield最简单的调用形式看起来像一个return申明,不同的是yield暂停当前过程的执行并返回值,而return是中断当前过程并返回值。暂停当前过程,意味着将处理权转交由上一级继续进行,直到上一级再次调用被暂停的过程,该过程又会从上一次暂停的位置继续执行。这像是什么呢?如果之前已经在鸟哥的文章中粗略看过,应该知道这很像操作系统的进程调度,多个进程在一个 CPU 核心上执行,在系统调度下每一个进程执行一段指令就被暂停,切换到下一个进程,这样外部用户看起来就像是同时在执行多个任务。 […]

  38. […] 本文使用php的yield模拟系统中的进程调度,主要参考于此篇文章:在PHP中使用协程实现多任务调度 具体的代码与其一致,主要针对其中的一些难点做出解释 首先看完上面这篇文章后,会让人产生不少疑惑的地方 1.协程(进程)到底是做什么的,作者基于yield实现了php内部的一个进程调度方案,这也是linux、windows等系统的调度方式 对于单核cpu来说,一样能够实现多任务,但其实cpu同一时间只能够运行一个任务,之所以看起来所有程序都在运行,是因为cpu不停的切换每一个任务,在每个任务里面运行一点点时间,就好比抄作业,现在有三门科目,语文数学英语,一直抄一门是最快的,但是你可以选择一天里,前八个小时抄语文,中间八个小时抄数学,最后八个小时抄英语,这样看上去就像是你一天抄了三门作业。 而这篇文章里面作者也是实现了这样的调度。 […]

  39. lobo
    lobo July 11, 2018

    logger 那个例子,如果我要手动fclose文件资源,怎么做呢?

  40. […] yield 最简单的调用形式看起来像一个 return 申明,不同的是 yield 暂停当前过程的执行并返回值,而 return 是中断当前过程并返回值。暂停当前过程,意味着将处理权转交由上一级继续进行,直到上一级再次调用被暂停的过程,该过程又会从上一次暂停的位置继续执行。这像是什么呢?如果之前已经在鸟哥的文章中粗略看过,应该知道这很像操作系统的进程调度,多个进程在一个 CPU 核心上执行,在系统调度下每一个进程执行一段指令就被暂停,切换到下一个进程,这样外部用户看起来就像是同时在执行多个任务。 […]

  41. […] 如果你有看過鳥哥的這篇文章http://www.laruence.com/2015/05/28/3038.html,應該對協程有個深刻的認識。但裡面內容更適合中高階PHP工程師看,而且還得具備一定的作業系統的知識,所以我在此基礎上用更通俗的方式,闡明一下PHP的協程概念。協程很強大的功能但相對比較複雜, 也比較難被理解。個人目前還沒有遇到合適的場景來使用PHP協程,不過我猜測,由於可以在使用者層面實現多併發,所以多用於CLI模式下的web服務開發,比如Golang的goroutine並不是執行緒,而是協程。還有yield有雙向通訊的功能,所以還可以實現非同步服務,但需要自己寫排程器,比如鳥哥這篇部落格裡面的非阻塞IOweb伺服器就是靠協程實現非同步了實現的。  […]

  42. […] 如果你之前了解过利用PHP实现协程的话,你肯定看过鸟哥的那篇文章:在PHP中使用协程实现多任务调度| 风雪之隅 […]

  43. ccnt
    ccnt May 28, 2018

    思想是能够理解,但代码实现看到系统调用就开始懵逼了,和大神的差距真是和狗的差距都大。。

    • noodles
      noodles May 16, 2019

      我比你好点,系统调用看懂了,到后面IO,堆栈懵逼了

  44. mayunyi
    mayunyi May 15, 2018

    function server($port) {
    echo “Starting server at port $port…\n”;
    $socket = @stream_socket_server(“tcp://localhost:$port”, $errNo, $errStr);
    if (!$socket) throw new Exception($errStr, $errNo);
    stream_set_blocking($socket, 0);
    while (true) {
    yield waitForRead($socket);
    $clientSocket = stream_socket_accept($socket, 0);
    yield newTask(handleClient($clientSocket));
    }
    }
    鸟哥 这一段代码我有一个疑问 循环里面每一次都要 yield waitForRead($socket); 这里是不是可以改进。 每次获取一个客户端的socket之前都要把服务端socket的server task 放到 $waitingForRead集合里面 在通过 ioPollTask 取出 server task 放回到 task队列。 这样每获取一个客户端socket的调度器至少要调度两次task。 如果并发很高的情况下 accept 队列应该有很多客户端socket,可以循环直接取出来的吧

  45. ma yunyi
    ma yunyi May 15, 2018

    这种简单的http服务器 不用协程是不是效率还会高点,

  46. […] PHP生成器Generator理解 2018年4月17日   –   0 comments   –   Tech Blog   转载整理自:寄凡、风雪之隅、PHP手册 […]

  47. […] 如果你有看過鳥哥的這篇文章http://www.laruence.com/2015/05/28/3038.html,應該對協程有個深刻的認識。但裡面內容更適合中高階PHP工程師看,而且還得具備一定的作業系統的知識,所以我在此基礎上用更通俗的方式,闡明一下PHP的協程概念。協程很強大的功能但相對比較複雜, 也比較難被理解。個人目前還沒有遇到合適的場景來使用PHP協程,不過我猜測,由於可以在使用者層面實現多併發,所以多用於CLI模式下的web服務開發,比如Golang的goroutine並不是執行緒,而是協程。還有yield有雙向通訊的功能,所以還可以實現非同步服務,但需要自己寫排程器,比如鳥哥這篇部落格裡面的非阻塞IOweb伺服器就是靠協程實現非同步了實現的。  […]

  48. su
    su May 9, 2018

    感谢鸟哥的分享。在“与调度器之间通信”这一节里面,小实例与上面的“没什么意义的例子”返回结果不一样,不是两个任务交替运行的,而是两个任务每两次再交替运行的。
    getTaskId函数中,实例化SystemCall的时候,将任务放入了调度器;run里面也将任务放入了调度器,导致调度器中出现连续两个相同的任务,所以输出是task1 task1 task2 task2…这样两次交替执行。

  49. 家居照明
    家居照明 April 8, 2018

    感觉大神们的分享也太强了。

  50. […]  【转】 BruceIT — PHP7下的协程实现前言相信大家都听说过『协程』这个概念吧。但是有些同学对这个概念似懂非懂,不知道怎么实现,怎么用,用在哪,甚至有些人认为yield就是协程!我始终相信,如果你无法准确地表达出一个知识点的话,我可以认为你就是不懂。如果你之前了解过利用PHP实现协程的话,你肯定看过鸟哥的那篇文章:在PHP中使用协程实现多任务调度| 风雪之隅鸟哥这篇文章是从国外的作者翻译来的,翻译的简洁明了,也给出了具体的例子了。我写这篇文章的目的,是想对鸟哥文章做更加充足的补充,毕竟有部分同学的基础还是不够好,看得也是云头雾里的。我个人,不喜欢写长篇文章,微博关注我 @码云 ,每天用微博分享知识。什么是协程先搞清楚,什么是协程。你可能已经听过『进程』和『线程』这两个概念。进程就是二进制可执行文件在计算机内存里的一个运行实例,就好比你的.exe文件是个类,进程就是new出来的那个实例。进程是计算机系统进行资源分配和调度的基本单位(调度单位这里别纠结线程进程的),每个CPU下同一时刻只能处理一个进程。所谓的并行,只不过是看起来并行,CPU事实上在用很快的速度切换不同的进程。进程的切换需要进行系统调用,CPU要保存当前进程的各个信息,同时还会使CPUCache被废掉。所以进程切换不到非不得已就不做。那么怎么实现『进程切换不到非不得已就不做』呢?首先进程被切换的条件是:进程执行完毕、分配给进程的CPU时间片结束,系统发生中断需要处理,或者进程等待必要的资源(进程阻塞)等。你想下,前面几种情况自然没有什么话可说,但是如果是在阻塞等待,是不是就浪费了。其实阻塞的话我们的程序还有其他可执行的地方可以执行,不一定要傻傻的等!所以就有了线程。线程简单理解就是一个『微进程』,专门跑一个函数(逻辑流)。所以我们就可以在编写程序的过程中将可以同时运行的函数用线程来体现了。线程有两种类型,一种是由内核来管理和调度。我们说,只要涉及需要内核参与管理调度的,代价都是很大的。这种线程其实也就解决了当一个进程中,某个正在执行的线程遇到阻塞,我们可以调度另外一个可运行的线程来跑,但是还是在同一个进程里,所以没有了进程切换。还有另外一种线程,他的调度是由程序员自己写程序来管理的,对内核来说不可见。这种线程叫做『用户空间线程』。协程可以理解就是一种用户空间线程。协程,有几个特点:协同,因为是由程序员自己写的调度策略,其通过协作而不是抢占来进行切换在用户态完成创建,切换和销毁*** ⚠️ 从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制***generator经常用来实现协程说到这里,你应该明白协程的基本概念了吧?PHP实现协程一步一步来,从解释概念说起!可迭代对象PHP5提供了一种定义对象的方法使其可以通过单元列表来遍历,例如用foreach语句。你如果要实现一个可迭代对象,你就要实现Iterator接口:<span class="hljs-meta"><?php</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyIterator</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Iterator</span></span>{    <span class="hljs-keyword">private</span> $var = <span class="hljs-keyword">array</span>();    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">($array)</span>    </span>{        <span class="hljs-keyword">if</span> (is_array($array)) {            <span class="hljs-keyword">$this</span>->var = $array;        }    }    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">rewind</span><span class="hljs-params">()</span> </span>{        <span class="hljs-keyword">echo</span> <span class="hljs-string">"rewindingn"</span>;        reset(<span class="hljs-keyword">$this</span>->var);    }    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">current</span><span class="hljs-params">()</span> </span>{        $var = current(<span class="hljs-keyword">$this</span>->var);        <span class="hljs-keyword">echo</span> <span class="hljs-string">"current: $varn"</span>;        <span class="hljs-keyword">return</span> $var;    }    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">key</span><span class="hljs-params">()</span> </span>{        $var = key(<span class="hljs-keyword">$this</span>->var);        <span class="hljs-keyword">echo</span> <span class="hljs-string">"key: $varn"</span>;        <span class="hljs-keyword">return</span> $var;    }    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">next</span><span class="hljs-params">()</span> </span>{        $var = next(<span class="hljs-keyword">$this</span>->var);        <span class="hljs-keyword">echo</span> <span class="hljs-string">"next: $varn"</span>;        <span class="hljs-keyword">return</span> $var;    }    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">valid</span><span class="hljs-params">()</span> </span>{        $var = <span class="hljs-keyword">$this</span>->current() !== <span class="hljs-keyword">false</span>;        <span class="hljs-keyword">echo</span> <span class="hljs-string">"valid: {$var}n"</span>;        <span class="hljs-keyword">return</span> $var;    }}$values = <sp
    an class="hljs-keyword">array</span>(<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>);$it = <span class="hljs-keyword">new</span> MyIterator($values);<span class="hljs-keyword">foreach</span> ($it <span class="hljs-keyword">as</span> $a => $b) {    <span class="hljs-keyword">print</span> <span class="hljs-string">"$a: $bn"</span>;}生成器可以说之前为了拥有一个能够被foreach遍历的对象,你不得不去实现一堆的方法,yield关键字就是为了简化这个过程。生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现Iterator接口的方式,性能开销和复杂性大大降低。<span class="hljs-meta"><?php</span><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">xrange</span><span class="hljs-params">($start, $end, $step = <span class="hljs-number">1</span>)</span> </span>{    <span class="hljs-keyword">for</span> ($i = $start; $i <= $end; $i += $step) {        <span class="hljs-keyword">yield</span> $i;    }} <span class="hljs-keyword">foreach</span> (xrange(<span class="hljs-number">1</span>, <span class="hljs-number">1000000</span>) <span class="hljs-keyword">as</span> $num) {    <span class="hljs-keyword">echo</span> $num, <span class="hljs-string">"n"</span>;}记住,一个函数中如果用了yield,他就是一个生成器,直接调用他是没有用的,不能等同于一个函数那样去执行!所以,yield就是yield,下次谁再说yield是协程,我肯定把你xxxx。PHP协程前面介绍协程的时候说了,协程需要程序员自己去编写调度机制,下面我们来看这个机制怎么写。0)生成器正确使用既然生成器不能像函数一样直接调用,那么怎么才能调用呢?方法如下:foreach他send($value)current / next…1)Task实现Task就是一个任务的抽象,刚刚我们说了协程就是用户空间协程,线程可以理解就是跑一个函数。所以Task的构造函数中就是接收一个闭包函数,我们命名为coroutine。<span class="hljs-comment">/** * Task任务类 */</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Task</span></span>{    <span class="hljs-keyword">protected</span> $taskId;    <span class="hljs-keyword">protected</span> $coroutine;    <span class="hljs-keyword">protected</span> $beforeFirstYield = <span class="hljs-keyword">true</span>;    <span class="hljs-keyword">protected</span> $sendValue;    <span class="hljs-comment">/**     * Task constructor.     * <span class="hljs-doctag">@param</span> $taskId     * <span class="hljs-doctag">@param</span> Generator $coroutine     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">($taskId, Generator $coroutine)</span>    </span>{        <span class="hljs-keyword">$this</span>->taskId = $taskId;        <span class="hljs-keyword">$this</span>->coroutine = $coroutine;    }    <span class="hljs-comment">/**     * 获取当前的Task的ID     *      * <span class="hljs-doctag">@return</span> mixed     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTaskId</span><span class="hljs-params">()</span>    </span>{        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>->taskId;    }    <span class="hljs-comment">/**     * 判断Task执行完毕了没有     *      * <span class="hljs-doctag">@return</span> bool     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isFinished</span><span class="hljs-params">()</span>    </span>{        <span class="hljs-keyword">return</span> !<span class="hljs-keyword">$this</span>->coroutine->valid();    }    <span class="hljs-comment">/**     * 设置下次要传给协程的值,比如 $id = (yield $xxxx),这个值就给了$id了     *      * <span class="hljs-doctag">@param</span> $value     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setSendValue</span><span class="hljs-params">($value)</span>    </span>{        <span class="hljs-keyword">$this</span>->sendValue = $value;    }    <span class="hljs-comment">/**     * 运行任务     *      * <span class="hljs-doctag">@return</span> mixed     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">run</span><span class="hljs-params">()</span>    </span>{        <span class="hljs-comment">// 这里要注意,生成器的开始会reset,所以第一个值要用current获取</span>        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">$this</span>->beforeFirstYield) {            <span class="hljs-keyword">$this</span>->beforeFirstYield = <span class="hljs-keyword">false</span>;            <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>->coroutine->current();        } <span class="hljs-keyword">else</span> {            <span class="hljs-comment">// 我们说过了,用sen
    d去调用一个生成器</span>            $retval = <span class="hljs-keyword">$this</span>->coroutine->send(<span class="hljs-keyword">$this</span>->sendValue);            <span class="hljs-keyword">$this</span>->sendValue = <span class="hljs-keyword">null</span>;            <span class="hljs-keyword">return</span> $retval;        }    }}2)Scheduler实现接下来就是Scheduler这个重点核心部分,他扮演着调度员的角色。<span class="hljs-comment">/** * Class Scheduler */</span><span class="hljs-class"><span class="hljs-keyword">Class</span> <span class="hljs-title">Scheduler</span></span>{    <span class="hljs-comment">/**     * <span class="hljs-doctag">@var</span> SplQueue     */</span>    <span class="hljs-keyword">protected</span> $taskQueue;    <span class="hljs-comment">/**     * <span class="hljs-doctag">@var</span> int     */</span>    <span class="hljs-keyword">protected</span> $tid = <span class="hljs-number">0</span>;    <span class="hljs-comment">/**     * Scheduler constructor.     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">()</span>    </span>{        <span class="hljs-comment">/* 原理就是维护了一个队列,         * 前面说过,从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制         * */</span>        <span class="hljs-keyword">$this</span>->taskQueue = <span class="hljs-keyword">new</span> SplQueue();    }    <span class="hljs-comment">/**     * 增加一个任务     *     * <span class="hljs-doctag">@param</span> Generator $task     * <span class="hljs-doctag">@return</span> int     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addTask</span><span class="hljs-params">(Generator $task)</span>    </span>{        $tid = <span class="hljs-keyword">$this</span>->tid;        $task = <span class="hljs-keyword">new</span> Task($tid, $task);        <span class="hljs-keyword">$this</span>->taskQueue->enqueue($task);        <span class="hljs-keyword">$this</span>->tid++;        <span class="hljs-keyword">return</span> $tid;    }    <span class="hljs-comment">/**     * 把任务进入队列     *     * <span class="hljs-doctag">@param</span> Task $task     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">schedule</span><span class="hljs-params">(Task $task)</span>    </span>{        <span class="hljs-keyword">$this</span>->taskQueue->enqueue($task);    }    <span class="hljs-comment">/**     * 运行调度器     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">run</span><span class="hljs-params">()</span>    </span>{        <span class="hljs-keyword">while</span> (!<span class="hljs-keyword">$this</span>->taskQueue->isEmpty()) {            <span class="hljs-comment">// 任务出队</span>            $task = <span class="hljs-keyword">$this</span>->taskQueue->dequeue();            $res = $task->run(); <span class="hljs-comment">// 运行任务直到 yield</span>            <span class="hljs-keyword">if</span> (!$task->isFinished()) {                <span class="hljs-keyword">$this</span>->schedule($task); <span class="hljs-comment">// 任务如果还没完全执行完毕,入队等下次执行</span>            }        }    }}这样我们基本就实现了一个协程调度器。你可以使用下面的代码来测试:<span class="hljs-meta"><?php</span><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">task1</span><span class="hljs-params">()</span> </span>{    <span class="hljs-keyword">for</span> ($i = <span class="hljs-number">1</span>; $i <= <span class="hljs-number">10</span>; ++$i) {        <span class="hljs-keyword">echo</span> <span class="hljs-string">"This is task 1 iteration $i.n"</span>;        <span class="hljs-keyword">yield</span>; <span class="hljs-comment">// 主动让出CPU的执行权</span>    }} <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">task2</span><span class="hljs-params">()</span> </span>{    <span class="hljs-keyword">for</span> ($i = <span class="hljs-number">1</span>; $i <= <span class="hljs-number">5</span>; ++$i) {        <span class="hljs-keyword">echo</span> <span class="hljs-string">"This is task 2 iteration $i.n"</span>;        <span class="hljs-keyword">yield</span>; <span class="hljs-comment">// 主动让出CPU的执行权</span>    }} $scheduler = <span class="hljs-keyword">new</span> Scheduler; <span class="hljs-comment">// 实例化一个调度器</span>$scheduler->newTask(task1()); <span class="hljs-comment">// 添加不同的闭包函数作为任务</span>$sche
    duler->newTask(task2());$scheduler->run();关键说下在哪里能用得到PHP协程。<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">task1</span><span class="hljs-params">()</span> </span>{        <span class="hljs-comment">/* 这里有一个远程任务,需要耗时10s,可能是一个远程机器抓取分析远程网址的任务,我们只要提交最后去远程机器拿结果就行了 */</span>        remote_task_commit();        <span class="hljs-comment">// 这时候请求发出后,我们不要在这里等,主动让出CPU的执行权给task2运行,他不依赖这个结果</span>        <span class="hljs-keyword">yield</span>;        <span class="hljs-keyword">yield</span> (remote_task_receive());        …} <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">task2</span><span class="hljs-params">()</span> </span>{    <span class="hljs-keyword">for</span> ($i = <span class="hljs-number">1</span>; $i <= <span class="hljs-number">5</span>; ++$i) {        <span class="hljs-keyword">echo</span> <span class="hljs-string">"This is task 2 iteration $i.n"</span>;        <span class="hljs-keyword">yield</span>; <span class="hljs-comment">// 主动让出CPU的执行权</span>    }}这样就提高了程序的执行效率。关于『系统调用』的实现,鸟哥已经讲得很明白,我这里不再说明。3)协程堆栈鸟哥文中还有一个协程堆栈的例子。我们上面说过了,如果在函数中使用了yield,就不能当做函数使用。所以你在一个协程函数中嵌套另外一个协程函数:<span class="hljs-meta"><?php</span><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">echoTimes</span><span class="hljs-params">($msg, $max)</span> </span>{    <span class="hljs-keyword">for</span> ($i = <span class="hljs-number">1</span>; $i <= $max; ++$i) {        <span class="hljs-keyword">echo</span> <span class="hljs-string">"$msg iteration $in"</span>;        <span class="hljs-keyword">yield</span>;    }} <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">task</span><span class="hljs-params">()</span> </span>{    echoTimes(<span class="hljs-string">'foo'</span>, <span class="hljs-number">10</span>); <span class="hljs-comment">// print foo ten times</span>    <span class="hljs-keyword">echo</span> <span class="hljs-string">"—n"</span>;    echoTimes(<span class="hljs-string">'bar'</span>, <span class="hljs-number">5</span>); <span class="hljs-comment">// print bar five times</span>    <span class="hljs-keyword">yield</span>; <span class="hljs-comment">// force it to be a coroutine</span>} $scheduler = <span class="hljs-keyword">new</span> Scheduler;$scheduler->newTask(task());$scheduler->run();这里的echoTimes是执行不了的!所以就需要协程堆栈。不过没关系,我们改一改我们刚刚的代码。把Task中的初始化方法改下,因为我们在运行一个Task的时候,我们要分析出他包含了哪些子协程,然后将子协程用一个堆栈保存。(C语言学的好的同学自然能理解这里,不理解的同学我建议去了解下进程的内存模型是怎么处理函数调用) <span class="hljs-comment">/**     * Task constructor.     * <span class="hljs-doctag">@param</span> $taskId     * <span class="hljs-doctag">@param</span> Generator $coroutine     */</span>    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">($taskId, Generator $coroutine)</span>    </span>{        <span class="hljs-keyword">$this</span>->taskId = $taskId;        <span class="hljs-comment">// $this->coroutine = $coroutine;</span>        <span class="hljs-comment">// 换成这个,实际Task->run的就是stackedCoroutine这个函数,不是$coroutine保存的闭包函数了</span>        <span class="hljs-keyword">$this</span>->coroutine = stackedCoroutine($coroutine);     }当Task->run()的时候,一个循环来分析:/** * @param Generator $gen */function stackedCoroutine(Generator $gen){    $stack = new SplStack;    // 不断遍历这个传进来的生成器    for (; 😉 {        // $gen可以理解为指向当前运行的协程闭包函数(生成器)        $value = $gen->current(); // 获取中断点,也就是yield出来的值        if ($value instanceof Generator) {            // 如果是也是一个生成器,这就是子协程了,把当前运行的协程入栈保存            $stack->push($gen);            $gen = $value; // 把子协程函数给gen,继续执行,注意接下来就是执行子协程的流程了            continue;        }        // 我们对子协程返回的结果做了封装,下面讲        $isReturnValue = $value instanceof CoroutineReturnValue; // 子协程返回`$value`需要主协程帮忙处理                if (!$gen->valid() || $isReturnValue) {            if ($stack->isEmpty()) {                return;            }            // 如果是gen已经执行完毕,或者遇到子协程需要返回值给主协程去处理            $gen = $stack->pop(); //出栈,得到之前入栈保存的主协程            $gen->send($isReturnValue ? $value->getValue() : NULL); // 调用主协程处理子协程的输出值            continue;        }        $gen->send(yield $gen->key() => $value); // 继续执行子协程    }}然后我们增加echoTime的结束标示:<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CoroutineReturnValue</span> </span>{ 
      <span class="hljs-keyword">protected</span> $value;     <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">($value)</span> </span>{        <span class="hljs-keyword">$this</span>->value = $value;    }         <span class="hljs-comment">// 获取能把子协程的输出值给主协程,作为主协程的send参数</span>    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getValue</span><span class="hljs-params">()</span> </span>{        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>->value;    }}<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">retval</span><span class="hljs-params">($value)</span> </span>{    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> CoroutineReturnValue($value);}然后修改echoTimes:<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">echoTimes</span><span class="hljs-params">($msg, $max)</span> </span>{    <span class="hljs-keyword">for</span> ($i = <span class="hljs-number">1</span>; $i <= $max; ++$i) {        <span class="hljs-keyword">echo</span> <span class="hljs-string">"$msg iteration $in"</span>;        <span class="hljs-keyword">yield</span>;    }    <span class="hljs-keyword">yield</span> retval(<span class="hljs-string">""</span>);  <span class="hljs-comment">// 增加这个作为结束标示</span>}Task变为:<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">task1</span><span class="hljs-params">()</span></span>{    <span class="hljs-keyword">yield</span> echoTimes(<span class="hljs-string">'bar'</span>, <span class="hljs-number">5</span>);}这样就实现了一个协程堆栈,现在你可以举一反三了。4)PHP7中yield from关键字PHP7中增加了yield from,所以我们不需要自己实现携程堆栈,真是太好了。把Task的构造函数改回去:    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">($taskId, Generator $coroutine)</span>    </span>{        <span class="hljs-keyword">$this</span>->taskId = $taskId;        <span class="hljs-keyword">$this</span>->coroutine = $coroutine;        <span class="hljs-comment">// $this->coroutine = stackedCoroutine($coroutine); //不需要自己实现了,改回之前的</span>    }echoTimes函数:<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">echoTimes</span><span class="hljs-params">($msg, $max)</span> </span>{    <span class="hljs-keyword">for</span> ($i = <span class="hljs-number">1</span>; $i <= $max; ++$i) {        <span class="hljs-keyword">echo</span> <span class="hljs-string">"$msg iteration $in"</span>;        <span class="hljs-keyword">yield</span>;    }}task1生成器:<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">task1</span><span class="hljs-params">()</span></span>{    <span class="hljs-keyword">yield</span> from echoTimes(<span class="hljs-string">'bar'</span>, <span class="hljs-number">5</span>);}这样,轻松调用子协程。总结这下应该明白怎么实现PHP协程了吧?End… 点赞 分享 […]

    • junita
      junita July 18, 2019

      it’s very difficult for me to understand maybe I should learn through the course

  51. eayy
    eayy February 2, 2018

    PHP7中生成器的新特性 yield-from && return-values

  52. […] 如果你之前了解过利用PHP实现协程的话,你肯定看过鸟哥的那篇文章:在PHP中使用协程实现多任务调度| 风雪之隅 […]

  53. a3f99c2d2c249997f80e12b665e3b0e3
    a3f99c2d2c249997f80e12b665e3b0e3 December 21, 2017

    感谢大神分享,在python中看到协程的感念,没想到php也可以实现,太牛

  54. Terry
    Terry November 16, 2017

    好反人类的语法:
    send(‘Foo’);
    $logger->send(‘Bar’)
    ?>
    一个没有return的函数,居然又可以执行方法,我靠,这什么写法

  55. lala
    lala November 15, 2017

    实在不晓得这个 系统调用 到底是咋回事 代码倒是看得通顺了 但很多描述看不懂 比如 “我们的任务调度系统将反映这种设计:不是简单地把调度器传递给任务(这样就允许它做它想做的任何事)” 把调度器传递给任务 什么鬼?

  56. johnny
    johnny October 26, 2017

    感谢大神分享,在python中看到协程的感念,没想到php也可以实现,太牛

  57. angelandy
    angelandy October 17, 2017

    看了5遍 硬是看不懂啊

  58. jz
    jz August 30, 2017

    为啥我ab -n 10000 -c 100 localhost:8000压测后只有100请求/秒,直接用阻塞的压测却能上10000请求/秒。。

  59. gaoziwen
    gaoziwen August 5, 2017

    迭代器生成器,不是迭代生成器,这用词,太难理解了

  60. buffer
    buffer July 18, 2017

    又臭又长!

  61. ailbs
    ailbs June 29, 2017

    看完整个人在懵逼状态,够我研究一段时间的了

  62. GPF
    GPF June 27, 2017

    为了看懂这篇文章顺便把 Iterator 的 demo 给敲了一遍,好高兴

  63. leo
    leo May 2, 2017

    task那个类好像有问题
    function task1() {
    for ($i = 1; $i run();
    这样,它只能输出一条
    This is task 1 iteration 1
    难道不是应该输出10条吗

  64. PHP代码注释 代码要写注释,这基本上是共识。PHP也有他的一套注视规范。 PHPDoc PHPDoc 是注释 PHP 代码的非正式标准。它有许多不同的标记可以使用。完整的标记列表和范例可以查看 PHPDoc 指南。 如下是撰…

  65. […] 其实在写本文之前,我对生成器以及基于此特性延伸出来的 php 的协程实现并没有比较直观的了解,主要是我个人水平并不是很高,属于典型的刚入了门的 PHPer。所以在看了前段时间鸟哥(laruence)博客中对协程的讲解(原文链接:《在PHP中使用协程实现多任务调度》)后,在我个人对本篇的理解上,针对那些比较难以理解的概念(包括我个人在理解这一概念的时候的难点),以一个更为通俗的方式去讲明白。当然由于本人也是刚刚去学习这一概念,所以有些不得当的地方在所难免,希望大神看见了请不吝赐教。 […]

  66. […] PHP中协程实现多任务调度,鸟哥有一篇翻译的文章里有讲解,网上能找到的大部分资料,都跟这篇相关。但是至少在我看来,理解起来还是蛮复杂的。这里针对那篇文章的前半部分做一个笔记,忽略后面关于独立堆栈协程的部分。 […]

  67. Tom
    Tom March 13, 2017

    协程还是太复杂了,用起来不方便

  68. Tom
    Tom March 9, 2017

    哦,知道了,是先调用的$iterator->current(),然后$iterator->next();

  69. Tom
    Tom March 9, 2017

    renwind的执行将会导致第一个yield被执行, 并且忽略了他的返回值,那foreach xrange 的第一个值为什么是1而不是2呢?

  70. log4geek.cc
    log4geek.cc February 25, 2017

    高级PHP技术交流群 11153486
    欢迎各位PHP同道中人加入交流技术。

  71. zzc
    zzc January 10, 2017

    php7支持yield from,那么stackedCoroutine就不需要了吧

  72. haowen
    haowen December 13, 2016

    这个应该就是异步非阻塞了吧,跟nginx的原理一样?

  73. cocktail
    cocktail October 27, 2016

    打错了是,nodejs 的 generator

  74. cocktail
    cocktail October 27, 2016

    感觉和nodejs的promise用法很像,看着略晕,,,

  75. forkxn
    forkxn August 4, 2016

    支持sky的说法,
    send(‘something’));
    // 如之前提到的在send之前, 当$gen迭代器被创建的时候一个renwind()方法已经被隐式调用
    // 所以实际上发生的应该类似:
    //$gen->rewind();
    //var_dump($gen->send(‘something’));
    //这样renwind的执行将会导致第一个yield被执行, 并且忽略了他的返回值.
    //真正当我们调用yield的时候, 我们得到的是第二个yield的值! 导致第一个yield的值被忽略.
    //string(3) “bar”
    这个默认执行rewind()是不成立的,手动调用rewind结果不一样,如果是默认调用应该也是一样的,所以英文原文估计也是错的。手册上原话:“如果当这个方法被调用时,生成器不在 yield 表达式,那么在传入值之前,它会先运行到第一个 yield 表达式”

  76. sky
    sky March 20, 2016

    看到系统调用一节,表示完全不懂了!

  77. Anonymous
    Anonymous January 29, 2016

    send(‘something’));
    // 如之前提到的在send之前, 当$gen迭代器被创建的时候一个rewind()方法已经被隐式调用
    // 所以实际上发生的应该类似:
    //$gen->rewind();
    //var_dump($gen->send(‘something’));
    //这样rewind的执行将会导致第一个yield被执行, 并且忽略了他的返回值.
    //真正当我们调用yield的时候, 我们得到的是第二个yield的值! 导致第一个yield的值被忽略.
    //string(3) “bar”
    其中的“这样rewind的执行将会导致第一个yield被执行, 并且忽略了他的返回值.” 描述有误(英文也是),在send之前调用current,即可返回第一个yield的值,并非rewind导致的忽略;查了手册,感觉像是send引起的,如下:
    Generator::send “如果当这个方法被调用时,生成器不在 yield 表达式,那么在传入值之前,它会先运行到第一个 yield 表达式。”,接着“向生成器中传入一个值,并且当做 yield 表达式的结果,然后继续执行生成器。”,即send之后会运行到第二个yield,并以“bar”作为返回值。

  78. showfuli
    showfuli January 9, 2016

    这个主题不就是google浏览器?!

  79. coach store online
    coach store online December 8, 2015

    The financial aspect is of course rather rosy but, that is not all they present to India. Wherever they go, in whatever capacity they go out of India almost all of them bring name to the Indian brain.
    coach store online http://coachhandbagsoutlet.hobo2015.com/

  80. 阿杜
    阿杜 December 8, 2015

    PHP5.5/PHP7 的生成器/协程用法和 Python 一样,在任何一个生成器调用位置都要写 yield,这对于上线较长时间的系统是非常麻烦的事情,要在很多地方加上 yield 才可以实现同步非阻塞的 IO 调用,为wsk 不用 lua 那种协程 —— 直接在函数调用堆栈的某个位置(比如子子函数调用)使用一次 yield 就可以了,这样子上线较长时间的系统只需要改动数据层就可以了,而且 lua 这种 yield 也能构建出生成器的效果!

  81. […] 其实在写本文之前,我对生成器以及基于此特性延伸出来的 php 的协程实现并没有比较直观的了解,主要是我个人水平并不是很高,属于典型的刚入了门的 PHPer。所以在看了前段时间鸟哥(laruence)博客中对协程的讲解(原文链接:《在PHP中使用协程实现多任务调度》)后,在我个人对本篇的理解上,针对那些比较难以理解的概念(包括我个人在理解这一概念的时候的难点),以一个更为通俗的方式去讲明白。当然由于本人也是刚刚去学习这一概念,所以有些不得当的地方在所难免,希望大神看见了请不吝赐教。 […]

  82. lucky
    lucky September 17, 2015

    killTask callback 写的有问题,无法为task中childTid 变量提供正确的参数,导致childTask() 死循环执行下去
    function newTask(\Generator $generator){
    return new SystemCall(function(Task $task,Scheduler $scheduler) use ($generator){
    $task->setSendValue($scheduler->newTask($generator)->getTaskId());
    $scheduler->schedule($task);
    });
    }
    注意:在scheduler->newTask(){ 最后一行加上 return $task;}

  83. […] 本文仅仅简单了解一下PHP的迭代生成器,它的复杂使用场景可以参考:http://www.laruence.com/2015/05/28/3038.html /*250*220 创建于 2015-06-07*/ var cpro_id = "u2143316"; […]

  84. baibing
    baibing July 6, 2015

    鸟哥 关于这里我有些疑问
    // 如之前提到的在send之前, 当$gen迭代器被创建的时候一个renwind()方法已经被隐式调用
    // 所以实际上发生的应该类似:
    //$gen->rewind();
    //var_dump($gen->send(‘something’));
    //这样renwind的执行将会导致第一个yield被执行, 并且忽略了他的返回值.
    //真正当我们调用yield的时候, 我们得到的是第二个yield的值! 导致第一个yield的值被忽略.
    如果是这样的话,那第一个例子中的应该1 也会被忽略了的呀?

  85. baibing
    baibing July 6, 2015

    php的协程,需要好好的看看~其中有一些不太明白,学习中

  86. reatang
    reatang July 6, 2015

    T.T看晕了

  87. 许胜斌
    许胜斌 June 9, 2015

    current()); // string(6) “yield1”
    var_dump($gen->send(‘ret1’)); // string(4) “ret1” (the first var_dump in gen)
    // string(6) “yield2” (the var_dump of the ->send() return value)
    var_dump($gen->send(‘ret2’)); // string(4) “ret2” (again from within gen)
    // NULL (the return value of ->send())
    ?>
    这个输出结果为啥是这样?

  88. Pigo Chu
    Pigo Chu June 6, 2015

    我試了一下,似乎不是真的非同步
    例如
    function task() {
    sleep(5);
    echo “hi\n”;
    yield;
    }
    $scheduler->newTask(task());
    $scheduler->newTask(task());
    $scheduler->run();
    我想讓 task() 暫停 5 秒後印出 hi , 同時跑兩個相同任務,但是實際結果是 5 秒後第一hi 印出,接著再5秒後第二個 hi 才出來,所以總共耗時 10 秒
    是不是我方法用錯了 ?

  89. pangou
    pangou June 6, 2015

    $gen->rewind(); // php5.6 rewind报错

  90. alonestar
    alonestar June 5, 2015

    题外话,为什么不考虑用其他语言解决协程,多任务的问题呢?
    然后用php和他们交互

  91. adam
    adam May 30, 2015

    汗,弄错了,博主帮删了吧。

  92. adam
    adam May 30, 2015

    第4段代码貌似少了个 var_dump($gen->current());

  93. darluc
    darluc May 29, 2015

    PHP还是得加油进化才行啊

  94. darluc
    darluc May 29, 2015

    PHP还是得加油进化才行啊

  95. kazaff
    kazaff May 29, 2015

    最经典的代码,就是那个“非阻塞IO”,需要有socket编程知识~
    PHP越来越吊炸天了啊~
    迭代生成器一开始我一直都不知道该怎么用是好啊~
    这个实现的调度器其实还是在单个进程里的,我理解的是,如果不考虑非阻塞IO的情况下,其实还是在串行的进行执行,只不过像鸟哥说的那样,交给调度器有秩序的进行执行时间片的安排~
    受益匪浅,顶鸟哥~

  96. 陈佳
    陈佳 May 29, 2015

    看到php越来越强大,越来越喜欢php,感谢各位开发者。

  97. 螃蟹在晨跑
    螃蟹在晨跑 May 28, 2015

    看了两遍,感觉足够研究一个月了。

  98. a
    a May 28, 2015

    聽說鳥哥的評論掛了!?

  99. Jenner
    Jenner May 28, 2015

    鸟哥觉得这个东西怎么样的?感觉语法和代码结构略复杂,用了怕代码可读性太差。
    其实说实话,真没看明白这东西除了在节省内存和CPU调度上,对性能有哪些提升

  100. Anonymous
    Anonymous May 28, 2015

    看了一下感觉白做了几年的phper,这个协程里面用的好多东西都没用过啊

  101. pakey
    pakey May 28, 2015

    消灭0回复

Comments are closed.