疑难解答——在PHP中使用协程实现多任务调度
本文使用php的yield模拟系统中的进程调度,主要参考于此篇文章:在PHP中使用协程实现多任务调度
具体的代码与其一致,主要针对其中的一些难点做出解释
首先看完上面这篇文章后,会让人产生不少疑惑的地方
1.协程(进程)到底是做什么的,作者基于yield实现了php内部的一个进程调度方案,这也是linux、windows等系统的调度方式
对于单核cpu来说,一样能够实现多任务,但其实cpu同一时间只能够运行一个任务,之所以看起来所有程序都在运行,是因为cpu不停的切换每一个任务,在每个任务里面运行一点点时间,就好比抄作业,现在有三门科目,语文数学英语,一直抄一门是最快的,但是你可以选择一天里,前八个小时抄语文,中间八个小时抄数学,最后八个小时抄英语,这样看上去就像是你一天抄了三门作业。
而这篇文章里面作者也是实现了这样的调度。
2.可能你会问,为什么要这么切换?一直运行一个程序不是最快的吗?
当然,一直运行一个程序是最快的,切换进程时的损耗虽然不大,但也不能忽视。
其中最主要的原因还是外部有许多慢速设备在进行连接,对于cpu来说,最快的自然是寄存器、一级缓存、二级和三级缓存了,对于它来说,内存都已经慢得无法忍受,更别提更慢的硬盘,以及其他io设备,例如键盘输入,网络请求等等,所以为了不让其浪费在时间在等待io上,就产生了这样一种运行方式。
3.作者的这个多任务调度看不懂
其实很简单,作者写了那么多类,其实就是完成类似这样的任务:
function test() { while(true) { echo "hello\n"; yield; echo "world\n"; yield; } } $test = test(); while (true) { $test->send(null); }
注意上面的两个yield,在真实系统中,起的就是中断的作用,当中断被处理后,会回到这里继续运行(如果觉得疑惑,可以了解下进程切换的原理,以及中断)
然后这样就会源源不断的打印出hello,world
接着又会产生上面的疑惑,这样写的意义何在?
跟直接写
function test() { while(true) { echo "hello\n"; echo "world\n"; } } test();
有什么区别?
正如上面所说,区别不在这里,在于等待慢速io上
如果采用下面这种写法,一旦当中出现了等待io的情况下,整个进程就会卡死,而在上面那种情况则不一样(当然真实系统中是抢占式调度,而不是这种主动让出来的模式)
例如,可以对比下面的代码
function wait() { sleep(2); echo "hello\n"; } function test() { while(true) { wait(); yield; echo "world\n"; yield; } } $test = test(); while (true) { $test->send(null); }
function wait() { sleep(2); echo "hello\n"; } function test() { while(true) { wait(); echo "world\n"; } } test();
我这里使用了sleep函数模拟等待io的情况下,注意现在这种情况并非真正的进程调度!
在这里他们的结果是相同的,运行速度也没有任何变化,不同之处在于如果使用了抢占式调度,假设每次调用yield前最大的执行时间为一秒
那么上面那段使用yield打印出来的将是
world 0s
world 1s
hello 2s
world 2s
world 3s
hello 4s
因为最开始wait函数分到的一秒钟被sleep消耗掉,第二次分掉的一秒钟也被消耗掉,所以才会在第三次打印,正如同上面一样,在4秒钟的时间里面,一种执行了六次,接下来我们看看第二种方式的调用
看到了吗?同样是4秒,但只执行了4次,这还只是简单的调用,要是换成其他的,能够节省更多的时间
sleep… 0s
sleep… 1s
sleep… 2s
hello 2s
world 2s
sleep… 3s
sleep… 4s
hello 4s
world 4s
4.系统调用中的这段代码看不懂
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); } ); }
这段代码其实很简单,主要就是添加了个新任务,同时继续执行当前任务
让人有点看不懂的可能在于setSendValue这一段
追踪这个函数,我们来到最上面
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; } }
可以看到,sendValue设置的值是会传送给迭代器中
那么哪里接收的呢?
newTask(task(10)); $scheduler->newTask(task(5)); $scheduler->run(); ?>
在这里,没想到吧!
看最上面的哪个yield,作者最开始的时候说过,yield这个关键字有两个意思,一个是用作接收send函数传进来的值,另外一个则是中断并返回当前值
所以当中的这段
$tid = (yield getTaskId()); // <-- here's the syscall!
并不能简单的认为
$tid = getTaskId();
如果你在getTaskId这个系统调用中写上return语句,$tid一样是无法接收的,因为系统调用最终的执行地点是在
if ($retval instanceof SystemCall) { $retval($task, $this); continue; }
这段代码中,而执行$retval这个回调函数的时候,是没有将返回值放到任何一个地方的!而且除了使用yield之外,是没有其他的方法能够将值传入迭代器中
所以
$tid = yield
这样写在一起看上有点迷惑,但实际问题不大,只要联系上下文,就能正确明白它的含义
这个问题解答暂时就到此为止了,在文章的最后作者也给出了个实际的案例:web服务器请求阻塞的解决方案,这样也算是把所学之物用上了吧,主要还是类似回调函数的解决方案,虽然表面是采用yield,实际上本质还是回调,只不过代码可以写成同步的结构