協(xié)程是比較少見的概念,先轉(zhuǎn)過來作為參考,需要時(shí)應(yīng)該可以用到
轉(zhuǎn)自:http://www.oschina.net/translate/cooperative-multitasking-using-coroutines-in-php
生成器
生成器最基本的思想也是一個(gè)函數(shù),這個(gè)函數(shù)的返回值是依次輸出,而不是只返回一個(gè)單獨(dú)的值?;蛘撸瑩Q句話說,生成器使你更方便的實(shí)現(xiàn)了迭代器接口。下面通過實(shí)現(xiàn)一個(gè)xrange函數(shù)來簡單說明:
<?php
<?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";
}
上面這個(gè)xrange()函數(shù)提供了和PHP的內(nèi)建函數(shù)range()一樣的功能。但是不同的是range()函數(shù)返回的是一個(gè)包含屬組值從1到100萬的數(shù)組(注:請查看手冊)。而xrange()函數(shù)返回的是依次輸出這些值的一個(gè)迭代器,而且并不會(huì)真正以數(shù)組形式計(jì)算。
這種方法的優(yōu)點(diǎn)是顯而易見的。它可以讓你在處理大數(shù)據(jù)集合的時(shí)候不用一次性的加載到內(nèi)存中。甚至你可以處理無限大的數(shù)據(jù)流。
當(dāng)然,也可以不同通過生成器來實(shí)現(xiàn)這個(gè)功能,而是可以通過繼承Iterator接口實(shí)現(xiàn)。通過使用生成器實(shí)現(xiàn)起來會(huì)更方便,而不用再去實(shí)現(xiàn)iterator接口中的5個(gè)方法了。
生成器為可中斷的函數(shù)
要從生成器認(rèn)識(shí)協(xié)同程序,理解它們內(nèi)部是如何工作的非常重要:生成器是可中斷的函數(shù),在它里面,yield構(gòu)成了中斷點(diǎn)。
緊接著上面的例子,如果你調(diào)用xrange(1,1000000)的話,xrange()函數(shù)里代碼沒有真正地運(yùn)行。相反,PHP只是返回了一個(gè)實(shí)現(xiàn)了迭代器接口的 生成器類實(shí)例:
<?php
$range = xrange(1, 1000000);
var_dump($range); // object(Generator)#1
var_dump($range instanceof Iterator); // bool(true)
協(xié)程
協(xié)程給上面功能添加的主要東西是回送數(shù)據(jù)給生成器的能力。這將把生成器到調(diào)用者的單向通信轉(zhuǎn)變?yōu)閮烧咧g的雙向通信。
<?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沒有作為一個(gè)語句來使用,而是用作一個(gè)表達(dá)式。即它有一個(gè)返回值。yield的返回值是傳遞給send()方法的值。 在這個(gè)例子里,yield將首先返回"Foo",然后返回"Bar"。
上面的例子里yield僅作為接收者。混合兩種用法是可能的,即既可接收也可發(fā)送。接收和發(fā)送通信如何進(jìn)行的例子如下:
<?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())
多任務(wù)協(xié)作
如果閱讀了上面的logger()例子,那么你認(rèn)為“為了雙向通信我為什么要使用協(xié)程呢? 為什么我不能只用常見的類呢?”,你這么問完全正確。上面的例子演示了基本用法,然而上下文中沒有真正的展示出使用協(xié)程的優(yōu)點(diǎn)。這就是列舉許多協(xié)程例子的理由。正如上面介紹里提到的,協(xié)程是非常強(qiáng)大的概念,不過這樣的應(yīng)用很稀少而且常常十分復(fù)雜。給出一些簡單而真實(shí)的例子很難。
多任務(wù)協(xié)作這個(gè)術(shù)語中的“協(xié)作”說明了如何進(jìn)行這種切換的:它要求當(dāng)前正在運(yùn)行的任務(wù)自動(dòng)把控制傳回給調(diào)度器,這樣它就可以運(yùn)行其他任務(wù)了。這與“搶占”多任務(wù)相反,搶占多任務(wù)是這樣的:調(diào)度器可以中斷運(yùn)行了一段時(shí)間的任務(wù),不管它喜歡還是不喜歡。協(xié)作多任務(wù)在Windows的早期版本(windows95)和Mac OS中有使用,不過它們后來都切換到使用搶先多任務(wù)了。理由相當(dāng)明確:如果你依靠程序自動(dòng)傳回 控制的話,那么壞行為的軟件將很容易為自身占用整個(gè)CPU,不與其他任務(wù)共享。
這個(gè)時(shí)候你應(yīng)當(dāng)明白協(xié)程和任務(wù)調(diào)度之間的聯(lián)系:yield指令提供了任務(wù)中斷自身的一種方法,然后把控制傳遞給調(diào)度器。因此協(xié)程可以運(yùn)行多個(gè)其他任務(wù)。更進(jìn)一步來說,yield可以用來在任務(wù)和調(diào)度器之間進(jìn)行通信。
我們的目的是 對 “任務(wù)”用更輕量級的包裝的協(xié)程函數(shù):
<?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();
}
}
任務(wù)ID標(biāo)記
<?php
function gen() {
yield 'foo';
yield 'bar';
}
$gen = gen();
var_dump($gen->send('something'));
// As the send() happens before the first yield there is an implicit rewind() call,
// so what really happens is this:
$gen->rewind();
var_dump($gen->send('something'));
// The rewind() will advance to the first yield (and ignore its value), the send() will
// advance to the second yield (and dump its value). Thus we loose the first yielded value!
調(diào)度器現(xiàn)在不得不比多任務(wù)循環(huán)要做稍微多點(diǎn)了,然后才運(yùn)行多任務(wù):
<?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);
}
}
}
}
<?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();
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.
與調(diào)度器之間通信
既然調(diào)度器已經(jīng)運(yùn)行了,那么我們就轉(zhuǎn)向日程表的下一項(xiàng):任務(wù)和調(diào)度器之間的通信。我們將使用進(jìn)程用來和操作系統(tǒng)會(huì)話的同樣的方式來通信:系統(tǒng)調(diào)用。我們需要系統(tǒng)調(diào)用的理由是操作系統(tǒng)與進(jìn)程相比它處在不同的權(quán)限級別上。因此為了執(zhí)行特權(quán)級別的操作(如殺死另一個(gè)進(jìn)程),就不得不以某種方式把控制傳回給內(nèi)核,這樣內(nèi)核就可以執(zhí)行所說的操作了。再說一遍,這種行為在內(nèi)部是通過使用中斷指令來實(shí)現(xiàn)的。過去使用的是通用的int指令,如今使用的是更特殊并且更快速的syscall/sysenter指令。
為了說明系統(tǒng)調(diào)用,我將對可調(diào)用的系統(tǒng)調(diào)用做一個(gè)小小的封裝:
<?php
class SystemCall {
protected $callback;
public function __construct(callable $callback) {
$this->callback = $callback;
}
public function __invoke(Task $task, Scheduler $scheduler) {
$callback = $this->callback; // Can't call it directly in PHP :/
return $callback($task, $scheduler);
}
}
<?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);
}
}
}
<?php
function getTaskId() {
return new SystemCall(function(Task $task, Scheduler $scheduler) {
$task->setSendValue($task->getTaskId());
$scheduler->schedule($task);
});
}
<?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();
<?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函數(shù)需要在調(diào)度器里增加一個(gè)方法:
<?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.