php5.5更好的新功能是支持发电机的支持和合作计划,发电机,PHP的文件和其他各种博客文章(比如这一个或这一个)已经非常详细。协作计划相对关注较少,难以解释和难以解释,但它具有强大的功能。

本文将引导您使用协作程序来实现任务调度,并通过实例实现对该技术的理解。我将在前三部分中简单介绍一下背景。如果您有更好的基础,您可以直接跳转到协作多任务部分。

发电机

发电机的最基本的理念是一个函数,这个函数的返回值是输出反过来,不仅是一个单一的价值。或者,换句话说,发电机使它为你实现迭代器接口更方便。下面是简单的功能实现了数据。

复制代码代码如下所示:
< PHP
函数xrange(美元开始,美元,美元一步= 1){
对于($ i = $开始;$ i = $结束;$ I = $步骤){
收益1美元;
}
}

foreach(xrange(1, 1000000)达数){
,;
}


上述xrange()函数提供了相同的功能的PHP内置函数,范围(),但不同的是,范围()函数返回一个数组包含一组值从1到100万(注:参见手册),xrange()函数返回一个迭代器,输出这些值又不是真的阵列形式的计数。

这种方法的优点是显而易见的,它可以让您在处理大型数据集时不必立即加载到内存中,甚至可以处理无限的数据流。

当然,它可以通过生成器来实现,但可以通过继承迭代器接口来实现,通过使用生成器,在迭代器接口中实现这5种方法更为方便。

发电机是可中断的功能。
从发电机中理解协作程序的功能是非常重要的,即发电机是一个可中断的函数,在该函数中,收益构成中断点。

在上面的例子中,如果你调用数据(11000000),在数据的代码()函数并没有真正运行。相反,PHP只是返回实现迭代器接口生成器类的一个实例:

复制代码代码如下所示:
< PHP
$范围=数据(1, 1000000);
var_dump(美元); / /对象(发电机)# 1
var_dump(美元范围是迭代器); / /布尔(真的)


一旦你打电话给一个对象的方法时,在代码运行一次。例如,如果你调用$范围->倒带(),在数据的代码()运行在控制流量首次出现产量的地方。在这种情况下,这意味着我跑的时候产量美元美元我= $开始。值传递给yield语句是用美元范围->电流()。

为了继续在生成执行代码,您必须调用$范围->下()方法,这将启动发电机直到yield语句出现。因此,通过调用下一个()和电流()方法不断,你就能得到所有的值从发电机直到一点不再现yield语句,xrange(),当我超过美元的end.in这种情况下出现这种情况,控制流将达到结束的功能,所以没有代码将被执行。一旦发生这种情况,最有效的()方法将返回false,当迭代结束。

协程

CO进程为上述函数添加的主要内容是将数据返回给生成器。这将将生成器和调用方之间的单向通信转换为两者之间的双向通信。
通过调用生成器的发送()方法而不是它的下一个()方法将数据传递给联合进程。

复制代码代码如下所示:
< PHP

函数记录器($文件名){
文件句柄= fopen(文件名美元美元,'a');
当(真){
fwrite($文件句柄,产量。;
}
}

日志记录器(__dir__ =美元。' /日志);
为记录->发送('foo);
为记录->发送('bar)


如您所见,这里的收益率不作为一个语句,而是作为一个表达式,也就是说,它有一个返回值。收益率的返回值是传递给发送()方法的值。

在上面的例子中,收益率只是接收者,可以混合两种用途,也就是说,两者都可以被接收并可以发送:

复制代码代码如下所示:
< PHP

函数根(){
ret =(yield'yield1美元);
var_dump($ RET);
ret =(yield'yield2美元);
var_dump($ RET);
}

$ = Gen();
var_dump(($创->电流) / /串);(6)产量
var_dump(美元创->发送('ret1 ')); / /串(4)ret1 (在创第一var_dump)
/ /串(6)的收益率2(的var_dump的->送(回)值)
var_dump(美元创->发送('ret2 ')); / /串(4)ret2 (再次在创)
空值(返回值)


它立刻理解输出的顺序的一个小小的困难,所以一定要确保你知道这么做,有两点我想特别指出的是:第一,圆括号对产量的表达双方的使用并非偶然。因为技术原因(虽然我已经考虑增加一个例外的分配,像Python),圆括号是必需的。其次,你可能已经注意到,倒带()不在呼叫电流称为()。如果这样做,倒带操作已经执行隐式。

多任务协作

如果您阅读上面的日志()示例,那么您认为我为什么要使用CO范围进行双向通信为什么我不能只使用普通类呢你是对的要求。上述例子表明的基本用法,但内容不真的表明使用协处理的优点。这是枚举有限范围的许多例子的原因。在介绍以上,合作过程是一个非常强大的概念,但是这样的应用是罕见的,通常非常复杂,给一些简单的实例是很困难的。

在这篇文章中,我要做的是用有限的过程中实现多任务协作。我们尝试解决的问题是,你想运行多个任务(或程序)兼任。但处理器一次只能运行一个任务(这篇文章的目的是不考虑那么多)。处理器需要不同的任务总是让每个任务运行一会儿之间切换。

在多任务合作期限合作说明开关是这样的:它要求当前运行的任务将控制返回给调度自动化,以便它可以运行其他任务。这是抢占式多任务相反,正是这种抢占多任务:调度器可以中断一个任务运行一一段时间,不管喜欢或不喜欢它。协同多任务应用在Windows的早期版本(Windows)和Mac OS,但他们后来都转向抢占式多任务。原因很清楚:如果你依靠程序自动发送控制,软件坏行为会占据整个CPU本身,它不与其他人共用的任务。

此时,您应该理解关联和任务调度之间的联系:屈服指令提供了一种中断任务本身的方法,然后将控制权传递给调度程序。

我们的目的是为任务使用更轻量级的协同过程函数包:

复制代码代码如下所示:
< PHP

课堂任务{
保护taskId美元;
美元的协同保护;
保护sendvalue美元= null;
保护beforefirstyield美元=真;

公共功能__construct($ taskId美元,发电机的协程){
美元-> taskId = $ taskId;
美元->协程= $协同;
}

公共功能gettaskid(){
返回这个-> taskId;
}

公共功能setsendvalue($ sendvalue){
美元-> sendvalue = $ sendvalue;
}

公共函数运行(){
如果(美元-> beforefirstyield){
美元-> beforefirstyield = false;
返回这个->访问->电流();
{人}
用美元=美元->程序->发送(美元-> sendvalue);
美元-> sendvalue = null;
返回用美元;
}
}

公共职能是(){
返回!美元->程序->有效();
}
}


一个任务是任务ID使用setsendvalue马克有限过程()方法,你可以指定的值将被发送到下一个回收(之后你会看到,我们需要这个),运行()函数不除合作程序,调用send()方法来做任何事情。明白为什么beforefirstyieldflag加入时,你需要考虑下面的代码片断:

复制代码代码如下所示:
< PHP

函数根(){
yield'foo;
yield'bar;
}

$ = Gen();
var_dump(美元创->发送());

当发送(发生)/在第一个输出之前,有一个隐式倒带(调用),
实际情况是这样的:
元- >倒带();
var_dump(美元创->发送());

倒带(前进)/前进到第一个收益率(忽略它的值),发送(威尔)
到第二个(产量前进/下降和它的价值,所以我们松散)。第一产值!


通过添加beforefirstyieldcondition,我们可以确定,第一屈服值。

调度程序现在必须比多任务循环做多一点,然后再运行多任务处理程序。

复制代码代码如下所示:
< PHP

类调度{
保护maxtaskid = 0美元;
保护taskmap美元= {}; / / taskId = >任务
保护taskqueue美元;

公共功能__construct(){
美元-> taskqueue =新splqueue();
}

公共职能任务(发电机有关美元){
$ TID = + +美元-> maxtaskid;
新的任务(任务=美元美元美元,tid,协程);
美元-> taskmap { $ TID } = $任务;
$ >附表(任务);
返回的TID;
}

公共功能调度(任务$任务){
这taskqueue美元-> ->入队($任务);
}

公共函数运行(){
而(!这taskqueue美元-> ->是空的()){
为任务=美元-> taskqueue -> dequeue();
任务>运行();

如果($任务->完成()){
unset(美元-> taskmap { $任务-> gettaskid())));
{人}
$ >附表(任务);
}
}
}
}


新的课题()方法(使用下一个空闲任务ID)创建一个新的任务,然后把任务到任务映射数组,然后实现任务把任务到任务队列的调度,然后运行()方法扫描任务队列运行任务。如果任务完成后,它将从队列中删除,否则将再次派出在队列的末尾。
让我们看一下下面的调度程序,其中有两个简单的(没有意义的)任务:

复制代码代码如下所示:
< PHP

功能任务(){
($ i = 1;$ i < 10;+ $ I){
这是任务1迭代。;
产量;
}
}

功能任务(){
($ i = 1;$ i < 5;+ $ I){
这是任务2迭代。;
产量;
}
}

$调度器=新调度器;

调度程序->新任务(任务1美元());
美元->任务(任务调度器());

计划程序>运行();


这两个任务都只显示一条消息,然后使用收益率将控制权返回给调度程序:

复制代码代码如下所示:
这是任务1迭代1。
这是任务2迭代1。
这是任务1迭代2。
这是任务2迭代2。
这是任务1迭代3。
这是任务2迭代3。
这是任务1迭代4。
这是任务2迭代4。
这是任务1迭代5。
这是任务2迭代5。
这是任务1迭代6。
这是任务1迭代7。
这是任务1迭代8。
这是任务1迭代9。
这是任务1迭代10。


输出确实是我们期望的:在前五个迭代中,这两个任务交替运行,然后第二个任务结束,只有第一个任务继续运行。

与调度器的通信

现在的程序正在运行,然后进入下一个项目:计划任务和调度之间的沟通。我们将在这过程中使用的操作系统课程的同一沟通方式:系统调用,我们需要系统调用的原因是操作系统是在一个不同的水平比较权威的过程。因此,为了执行特权操作(如杀人的过程),我们必须控制后以某种方式的核心,使内核可以执行的操作。再次,这种行为是通过中断指令内部实现。在过去,采用常见的int指令,现在他们用一种更特殊和更快的系统调用/市亿盛中心教学。

我们的任务调度系统将反映这一设计:不是简单地传输调度任务(只要他们可以做任何他们想做的事情),我们将与系统传递信息到产量表达式调用。这里产量中断和传递信息的一种方式来调度程序(和传出来的信息从调度)。

为了演示系统调用,我将对可调用的系统调用进行一个小封装:

复制代码代码如下所示:
< PHP

类系统调用{
受保护的$回调;

公共函数(调用回调__construct美元){
此回调;
}

公共功能__invoke(任务任务调度器调度器美元,美元){
$回调=这个回调函数不能直接调用它:
返回$回调($任务,$调度器);
}
}


它会像任何其他可调用的方法(使用_invoke),但它需要调度程序通过调用任务和本身的功能。为了解决这个问题,我们必须修改运行的调度方法略。

复制代码代码如下所示:
< PHP
公共函数运行(){
而(!$this->taskQueue->isEmpty ()) {
为任务=美元-> taskqueue -> dequeue();
用美元=美元任务->运行();

如果(用美元是系统调用){
用美元(美元的任务,美元);
继续;
}

如果($任务->完成()){
unset(美元-> taskmap { $任务-> gettaskid())));
{人}
$ >附表(任务);
}
}
}


第一个系统调用除了返回到任务ID之外没有其他操作。

复制代码代码如下所示:
< PHP
功能gettaskid(){
返回新的系统调用(函数(任务任务调度器调度器美元,美元){
任务-> setsendvalue美元(美元任务-> gettaskid());
$调度器>调度(任务);
});
}


这个函数确实为下一个值设置任务ID,任务再次被分派。由于使用系统调用,调度器不自动调用任务,我们需要手动调度任务(稍后您将了解原因):

复制代码代码如下所示:
< PHP

函数任务($ max){
1美元=(产量)(gettaskid); / /这里的系统调用!
($ i = 1;$ i $ max;+ $ I){
回声这是任务组迭代I 美元美元;
产量;
}
}

$调度器=新调度器;

$调度->任务(任务(10));
$调度->任务(任务(5));

计划程序>运行();


此代码将提供与前面示例相同的输出。请注意,系统调用与其他调用一样正常运行,但收益率是预先添加的。要创建新任务,然后再次杀死它们,需要两个以上的系统调用:
复制代码代码如下所示:
< PHP

功能任务(发电机有关美元){
返回新的系统调用(
功能(任务任务调度器调度器美元,美元(美元)使用协程){
任务-> setsendvalue美元($调度->任务(合协程));
$调度器>调度(任务);
}
);
}

功能killtask($ TID){
返回新的系统调用(
功能(任务任务调度器调度器美元,美元(美元)使用TID){
任务-> setsendvalue美元($调度-> killtask($ TID));
$调度器>调度(任务);
}
);
}


的killtask功能需要添加一个方法来调度:

复制代码代码如下所示:
< PHP

公共功能killtask($ TID){
如果(!isset(美元-> taskmap { $ TID })){
返回false;
}

unset(美元-> taskmap { $ TID });

这有点丑,可以优化,所以不需要排队,
但是假设杀人任务很少见,我现在就不去麻烦了。
foreach(美元-> taskqueue达我= $任务){
如果($任务-> gettaskid)(= $ TID){
unset(美元-> taskqueue {我});
打破;
}
}

返回true;
}