Example of an iterative generator for PHP5.5

Source: Internet
Author: User
Tags foreach error handling event listener fread generator getmessage socket strlen

PHP5.5 introduces the concept of an iterative generator, the concept of iteration is already in PHP, but the iterative generator is a new feature of PHP, similar to the iterative generator in Python3, to see how the PHP5.5 Iteration Builder is defined.

<?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";
}

Note the keyword: yield, it is this yeild keyword that constructs an iterator, this function xrange the difference from the previous function is here. The general situation is return a value, and yield a value means that this is an iterator, no loop once this iterator generates this value, so called an iterative generator, the iterative generator This function can be a foreach loop, each time produces a value.

PHP5.5 is a way to construct an iterator by defining a class to implement the iterator interface, which will improve performance and save system overhead by yield the construction iterator.

The advantages of this approach are obvious. It allows you to handle large data sets without having to load them into memory at once, or even to handle infinitely large streams of data.

As the example above shows, the function of this iterator is to generate numbers from 1 to 1000000, loop output, then using the previous approach is to generate a good 1 to 1000000 of the number into the array, will be very memory, because it is necessary to generate all the results in advance, rather than the time required to generate, That is, when you call xrange this iterator, the inside function does not actually run until you iterate each iteration.

Take a look at the PHP website example:


<?php
function xrange ($start, $limit, $step = 1) {
for ($i = $start; $i <= $limit; $i = = $step) {
Yield $i;
}
}

echo ' single digit odd numbers: ';

/*
* Note this is never created or returned,
* Which saves memory.
*/
foreach (Xrange (1, 9, 2) as $number) {
echo "$number";
}

echo "\ n";
?>

The xrange here is an iteration, function and range is the same, if the range function, then the internal implementation of the function will store the intermediate process of each iteration, that is, each intermediate variable has a memory space, then the first program to use a large memory space, and allocate memory, Reclaiming memory can cause the program to run longer. But if you use the Xrange function implemented on the yield, all of the intermediate variables in it will use only one memory $i, which will save you less time and space.

So why does yield have this effect? Lenovo's yield in Lua is a concept of the coprocessor. In the Lua language, when a program runs to yield, it records the context environment using a coprocessor, and then returns the program operation right to the main function, and when the main function calls resume, the process is revived and the context of the yield record is read. This forms a program language-level multi-process operation. PHP 5.5 Here yield is also the same truth, when the program runs to yield, the current program to evoke the context of the process, and then the main function continues to operate, but PHP does not use such as resume keyword, but "in the use of the time to arouse the" coprocessor. For example, the foreach iterator in the previous example can evoke yield. So this example above can be understood.


Iteration Builder

The (Iteration) generator is also a function, except that the return value of this function is returned sequentially instead of returning a single value. Or, in other words, the generator makes it easier for you to implement the iterator interface. The following is a simple description by implementing a xrange function:

<?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";
}
The above xrange () function provides the same functionality as the PHP built-in function range (). But the range () function returns an array containing values ranging from 1 to 1 million (note: Check the manual). The xrange () function returns an iterator that sequentially outputs the values, rather than actually returning them as an array.

The advantages of this approach are obvious. It allows you to work with large data sets without having to load them into memory at once. You can even handle an infinitely large stream of data.

Of course, it can be done differently through the generator, but it can be implemented by inheriting the iterator interface. But it is easier to implement it by using the generator, without having to implement the 5 methods in the iterator interface.

Generator is an interruptible function

It is important to understand how the process works internally from the generator, and it is essential that the generator is an interruptible function in which the yield form the interrupt point.

Or look at the example above, when invoking Xrange (1,1000000), the code in the Xrange () function does not actually run. It simply returns an iterator:

<?php
$range = xrange (1, 1000000);
Var_dump ($range); Object (Generator) #1
Var_dump ($range instanceof iterator); BOOL (TRUE)
?>
This also explains why Xrange is called an iterative generator because it returns an iterator, which implements the iterator interface.

The method of calling the iterator is once, where the code runs once. For example, if you call $range->rewind (), the Code in Xrange () runs to where the control flow first appears yield. The return value passed to the yield statement within a function can be obtained by $range->current ().

To continue executing the yield code in the generator, you need to invoke the $range->next () method. This will start the generator again until the next yield statement appears. Therefore, successive calls to the next () and current () methods enable you to get all the values from the generator until no yield statements appear.

For Xrange (), this situation occurs when $i exceeds $end. In this case, the control flow will reach the end of the function, so no code will be executed. Once this happens, the Vaild () method returns False, at which point the iteration ends.

Co-process

The support for the coprocessor is based on an iterative generator that adds the ability to send data back to the generator (the caller sends the data to the called generator function). This translates the generator to the caller's one-way communication into two-way communication between the two.

The ability to pass data is implemented through the Send () method of the iterator. The following logger () coprocessor is an example of how this communication works:

<?php
function Logger ($fileName) {
$fileHandle = fopen ($fileName, ' a ');
while (true) {
Fwrite ($fileHandle, yield. "\ n");
}
}

$logger = Logger (__dir__. '/log ');
$logger->send (' Foo ');
$logger->send (' Bar ')
?>
As you can see, here yield is not used as a statement, but as an expression, that is, it can be evolved into a value. This value is the value that the caller passes to the Send () method. In this example, the yield expression will first be replaced by "Foo" and written to log, and then "Bar" instead of writing to log.

The example above shows yield as the receiver, and then we look at the examples of how to receive and send at the same time:

&lt;?php


Function Gen () {


$ret = (yield ' yield1 ');


Var_dump ($ret);


$ret = (yield ' yield2 ');


Var_dump ($ret);


}





$gen = Gen ();


Var_dump ($gen-&gt;current ()); String (6) "Yield1"


Var_dump ($gen-&gt;send (' Ret1 ')); String (4) "Ret1" (the Var_dump in Gen)


String (6) "Yield2" (the Var_dump of the-&gt;send () return value)


Var_dump ($gen-&gt;send (' Ret2 ')); String (4) "Ret2" (again from within Gen)


NULL (The return value of-&gt;send ())


?&gt;


It may be a little difficult to quickly understand the exact order of the output, but you are sure to figure out why it is output in this way. For further reading.

In addition, I would like to highlight two points:

1th, the parentheses on either side of the yield expression are not optional before PHP7, meaning that parentheses are necessary in PHP5.5 and PHP5.6.

2nd, you may have noticed that rewind () was not invoked before calling current (). This is because the rewind operation has been implicitly performed when the iteration object is generated.

Multi-Task collaboration

If you read the logger () example above, you may wonder "Why should I use a coprocessor for two-way communication?" Can I use other non-coprocessor methods to do the same thing? "Yes, you're right, but the above example is just to illustrate the basic usage, and this example doesn't really show the benefits of using the coprocessor."

As mentioned in the previous introduction, the coprocessor is a very powerful concept, but it is very rare and often complex. It is difficult to give some simple and real examples.

In this article, what I decided to do is to use the coprocessor to achieve multitasking. The problem we are trying to solve is that you want to run multitasking (or "programs") concurrently. But we all know that the CPU can only run one task at a time (regardless of multi-core). So the processor needs to switch between different tasks, And always let each task run "a little while".

The term "collaboration" in multitasking is a good illustration of how to switch: it requires that currently running tasks automatically pass control back to the scheduler, so that other tasks can be run. This is in contrast to preemption multitasking: A scheduler can interrupt a task that runs for a period of time, whether it likes it or not. Collaborative multitasking is used in earlier versions of Windows (WINDOWS95) and Mac OS, but they later switch to using preemptive multitasking. The reason is quite clear: if you rely on the program to automatically surrender control, then some malicious programs will easily occupy the entire CPU, not with other tasks to share.

Now you should understand the relationship between the coprocessor and the Task Scheduler: The yield directive provides a way for the task to interrupt itself, and then the control is handed back to the Task Scheduler. Therefore, the coprocessor can run several other tasks. Further, yield can also be used to communicate between tasks and dispatchers.

In order to implement our multitask scheduling, we first implement the "task"-a wrapper function with lightweight packaging:

&lt;?php


Class Task {


protected $taskId;


protected $coroutine;


protected $sendValue = null;


protected $beforeFirstYield = true;





Public function __construct ($taskId, generator $coroutine) {


$this-&gt;taskid = $taskId;


$this-&gt;coroutine = $coroutine;


}





Public Function GetTaskID () {


return $this-&gt;taskid;


}





Public Function Setsendvalue ($sendValue) {


$this-&gt;sendvalue = $sendValue;


}





Public Function run () {


if ($this-&gt;beforefirstyield) {


$this-&gt;beforefirstyield = false;


return $this-&gt;coroutine-&gt;current ();


} else {


$retval = $this-&gt;coroutine-&gt;send ($this-&gt;sendvalue);


$this-&gt;sendvalue = null;


return $retval;


}


}





Public Function isfinished () {


Return! $this-&gt;coroutine-&gt;valid ();


}


}


As with code, a task is a covariant (function) marked with a task ID. Using the Setsendvalue () method, you can specify which values will be sent to the next recovery (you will understand that we need this later), and the run () function does not do anything except to invoke the "send" method's synergistic program, to understand why the addition of a Beforefirstyieldflag variables, you need to consider the following code fragment:

<?php
Function Gen () {
Yield ' foo ';
Yield ' bar ';
}

$gen = Gen ();
Var_dump ($gen->send (' something '));

As mentioned before send, a Renwind () method has been implicitly invoked when the $gen iterator is created.
So what actually happens should be similar to this:
$gen->rewind ();
Var_dump ($gen->send (' something '));

This renwind execution will cause the first yield to be executed and ignores his return value.
Really when we call yield, we get the value of the second yield! Causes the value of the first yield to be ignored.
String (3) "Bar"
By adding beforefirstyieldcondition we can determine that the value of the first yield can be returned correctly.

The scheduler now has to do a little bit more than a multitasking loop before running multiple tasks:

&lt;?php


Class Scheduler {


protected $maxTaskId = 0;


protected $TASKMAP = []; TaskId =&gt; Task


protected $taskQueue;





Public Function __construct () {


$this-&gt;taskqueue = new Splqueue ();


}





Public function NewTask (generator $coroutine) {


$tid = + + $this-&gt;maxtaskid;


$task = new Task ($tid, $coroutine);


$this-&gt;taskmap[$tid] = $task;


$this-&gt;schedule ($task);


return $tid;


}





Public Function Schedule (Task $task) {


$this-&gt;taskqueue-&gt;enqueue ($task);


}





Public Function run () {


while (! $this-&gt;taskqueue-&gt;isempty ()) {


$task = $this-&gt;taskqueue-&gt;dequeue ();


$task-&gt;run ();





if ($task-&gt;isfinished ()) {


unset ($this-&gt;taskmap[$task-&gt;gettaskid ()]);


} else {


$this-&gt;schedule ($task);


}


}


}


}


?&gt;


The NewTask () method creates a new task with the next idle task ID, and then puts the task in the task map array. Then it realizes the task scheduling by putting the task into the task queue. Then the run () method scans the task queue and runs the task. If a task is finished, it will be removed from the queue, or it will be scheduled again at the end of the queue.

Let's look at the following scheduler with two simple (meaningless) tasks:

<?php
Function Task1 () {
    for ($i = 1; $i <= + + $i) {
&NBSP;&NBSP;&NBSP;&NBSP;&N bsp;   echo "This is Task 1 iteration $i. \ n";
        yield;
   }
}
 
function Task2 () {
    for ($i = 1; $i <= 5; + $i) {
   &nb sp;    echo "This is Task 2 iteration $i. \ n";
        yield;
   }
}
 
$scheduler = new Scheduler
 
$scheduler->newtask (Task1 ());
$scheduler-> NewTask (Task2 ());
 
$scheduler->run ();
All two tasks echo only one message, and then use yield to pass control back to the scheduler. The output is as follows:

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.
The output is indeed as we expected: for the first five iterations, the two tasks were run alternately, and only the first task continued to run after the second task ended.

Communicating with the Scheduler

Now that the scheduler is running, let's look at the next problem: the communication between the task and the scheduler.

We will use processes to communicate in the same way as the operating system session: System calls.

The reason we need a system call is that the operating system is at a different level of authority than the process. So in order to perform privileged operations (such as killing another process), you have to somehow pass control back to the kernel so that the kernel can do what it says. Again, this behavior is implemented internally through the use of interrupt directives. Used to be generic int directives, now using more specific and faster syscall/sysenter directives.

Our Task Scheduler system will reflect this design: instead of simply passing the scheduler to the task (which allows it to do whatever it wants), we will communicate with the system call by passing information to the yield expression. Here yield is a break, and a way to pass information to the scheduler (and to pass information from the scheduler).

To illustrate the system call, we do a small encapsulation of callable system calls:

<?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);
}
}
It runs like any other callable object (using _invoke), but it requires the scheduler to pass the calling task and itself to the function.

To solve this problem we have to slightly modify the scheduler's Run method:

&lt;?php


Public Function run () {


while (! $this-&gt;taskqueue-&gt;isempty ()) {


$task = $this-&gt;taskqueue-&gt;dequeue ();


$retval = $task-&gt;run ();





if ($retval instanceof Systemcall) {


$retval ($task, $this);


Continue


}





if ($task-&gt;isfinished ()) {


unset ($this-&gt;taskmap[$task-&gt;gettaskid ()]);


} else {


$this-&gt;schedule ($task);


}


}


}


The first system call did nothing but return the task ID:

<?php
Function GetTaskID () {
    return new Systemcall (function (Task $task, Scheduler $ Scheduler) {
        $task->setsendvalue ($task->gettaskid ());
        $scheduler->schedule ($task);
   });
}
This function sets the value of the task ID to be sent the next time, and dispatches the task again. Because system calls are used, the scheduler does not automatically invoke the task, and we need to manually schedule the task (you will later understand why). To use this new system call, we'll rewrite the previous example:

<?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 ();
?>
This code will give the same output as the previous example. Notice how the system call works as normal as any other call, except that the yield is added in advance.

More than two system calls are required to create new tasks and then kill them:

&lt;?php


function NewTask (generator $coroutine) {


return new Systemcall (


function (Task $task, Scheduler $scheduler) use ($coroutine) {


$task-&gt;setsendvalue ($scheduler-&gt;newtask ($coroutine));


$scheduler-&gt;schedule ($task);


}


);


}





function Killtask ($tid) {


return new Systemcall (


function (Task $task, Scheduler $scheduler) use ($tid) {


$task-&gt;setsendvalue ($scheduler-&gt;killtask ($tid));


$scheduler-&gt;schedule ($task);


}


);


}


The Killtask function needs to add a method to the scheduler:

<?php
Public Function Killtask ($tid) {
    if (!isset ($this->taskmap[$tid)) {
        return false;
   }
 
    unset ($this->taskmap[$tid]);
 
   //This is a bit UGL Y 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) {
&nb sp;       if ($task->gettaskid () = = $tid) {
             unset ($this->taskqueue[$i]);
            break;
       }
   }
 
    return true;
}
The micro script used to test the new functionality:

<?php
Function Childtask () {
    $tid = (yield gettaskid ());
    while (t Rue) {
        echo "Child task $tid still alive!\n";
         yield;
   }
}
 
Function Task () {
    $tid = (yield gettaskid ());
    $childTi D = (yield NewTask (Childtask ()));
 
    for ($i = 1; $i <= 6; + + $i) {
        echo P Arent task $tid iteration $i. \ n ";
        yield;
 
        if ($i = = 3) yield killtask ($childTid);
    }
}
 
$scheduler = new Scheduler
$scheduler->newtask (Task ());
$scheduler->run ();

The code prints the following information:

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.
The subtasks will be killed after three iterations, so this is the end of the "child is still alive" message. But you have to understand that this is not really a parent-child relationship. Because subtasks can still run after the parent task ends, the subtasks can even kill the parent task. You can modify the scheduler so that it has a more hierarchical task structure, but this is not the scope of our article to continue the discussion.

Now you can implement many process management calls. For example, wait (it waits until the task finishes running), EXEC (it replaces the current task) and fork (it creates a clone of the current task). Fork is very cool, and you can actually implement it using the PHP coprocessor because they all support cloning.

Let's leave that to interested readers and let's take a look at the next issue.

Non-blocking IO

Obviously, the really cool application of our task management system should be the Web server. It has a task to listen on a socket for a new connection, and when a new connection is to be established, it creates a new task to process the new connection.

The hardest part of the Web server is usually the blocking of socket operations like read data. PHP, for example, waits until the client completes sending. This is a little less efficient for a Web server. Because the server can only handle one connection at a point in time.

The solution is to ensure that the socket is "ready" before it is actually read and written to the socket. To find out which socket is ready to read or write, you can use the stream selection function.

First, let's add two new syscall that will wait until the specified socket is ready:

<?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);
}
);
}
These syscall only represent their own methods in the scheduler:

&lt;?php





ResourceID =&gt; [Socket, tasks]


protected $waitingForRead = [];


protected $waitingForWrite = [];





Public Function Waitforread ($socket, Task $task) {


if (Isset ($this-&gt;waitingforread[(int) $socket]) {


$this-&gt;waitingforread[(int) $socket][1][] = $task;


} else {


$this-&gt;waitingforread[(int) $socket] = [$socket, [$task]];


}


}





Public Function Waitforwrite ($socket, Task $task) {


if (Isset ($this-&gt;waitingforwrite[(int) $socket]) {


$this-&gt;waitingforwrite[(int) $socket][1][] = $task;


} else {


$this-&gt;waitingforwrite[(int) $socket] = [$socket, [$task]];


}


}


The Waitingforread and Waitingforwrite properties are an array of two host waiting sockets and the tasks that wait for them. The interesting part is the following method, which checks to see if the socket is available and reschedule the respective tasks:

&lt;?php





protected function Iopoll ($timeout) {


$rSocks = [];


foreach ($this-&gt;waitingforread as List ($socket)) {


$rSocks [] = $socket;


}





$wSocks = [];


foreach ($this-&gt;waitingforwrite as List ($socket)) {


$wSocks [] = $socket;


}





$eSocks = []; Dummy





if (!stream_select ($rSocks, $wSocks, $eSocks, $timeout)) {


Return


}





foreach ($rSocks as $socket) {


List (, $tasks) = $this-&gt;waitingforread[(int) $socket];


Unset ($this-&gt;waitingforread[(int) $socket]);





foreach ($tasks as $task) {


$this-&gt;schedule ($task);


}


}





foreach ($wSocks as $socket) {


List (, $tasks) = $this-&gt;waitingforwrite[(int) $socket];


Unset ($this-&gt;waitingforwrite[(int) $socket]);





foreach ($tasks as $task) {


$this-&gt;schedule ($task);


}


}


}


The Stream_select function accepts arrays that host read, write, and pending sockets (we do not need to consider the last category). The array is passed by reference, and the function retains only those array elements whose state has changed. We can iterate over these arrays and reschedule the tasks associated with them.

In order to perform the above polling action normally, we will add a special task to the Scheduler:

<?php
protected function Iopolltask () {
while (true) {
if ($this->taskqueue->isempty ()) {
$this->iopoll (NULL);
} else {
$this->iopoll (0);
}
Yield
}
}
?>
You need to register for this task somewhere, for example, you can add $this->newtask ($this->iopolltask ()) at the start of the run () method. And then just like any other task. Perform a polling operation once for each complete task loop (this is not the best method), Iopolltask will use a 0-second timeout to invoke Iopoll, that is, Stream_select will return immediately (instead of waiting).

We use a null timeout only if the task queue is empty, which means that it waits until a set of interfaces is ready. If we do not, then the polling task will run again and again until a new connection is established. This will result in 100% CPU utilization. Instead, it is more efficient to have the operating system do this wait.

It's relatively easy to write a server now:

&lt;?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 = &lt;&lt;&lt;res


http/1.1 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-&gt;newtask (Server (8000));


$scheduler-&gt;run ();


This code implements the connection on the receiving localhost:8000, and then returns the sent content as an HTTP response. Of course it can handle really complex HTTP requests, and the code snippet above just demonstrates the general concept.

You can use a command like Ab-n 10000-c localhost:8000/to test the server. This command will send 10,000 requests to the server, and 100 of them will arrive at the same time. With this number, I get a response time of 10 milliseconds in the middle. But there is one problem: a handful of requests are really slow (like 5 seconds), which is why total throughput is only 2000 Requests/sec (if the response time is 10 milliseconds, the total throughput should be more like 10000 requests/sec)

Coprocessor Stack

If you try to build a larger system with our scheduling system, you'll soon have a problem: we're used to breaking code into smaller functions and calling them. However, if you use a coprocessor, you cannot do so. For example, look at the following code:

<?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 ();
This code attempts to embed the repeated loop "Output n Times" code into a separate coprocessor and then invoke it from the main task. However, it cannot run. As mentioned in the beginning of this article, the Call Builder (or the coprocessor) will not really do anything, it simply returns an object. This also appears in the example above: The Echotimes call does nothing but put back a (useless) covariant object.

In order to still allow this, we need to write a small encapsulation on this bare thread. We'll call it: "The coprocessor stack." Because it will manage the nested process call stack. This will be possible through the generation of a coprocessor calling a sub-coprocessor:

$retval = (yield Somecoroutine ($foo, $bar));
With yield, the child can return the value again:

Yield retval ("I ' m a return value!");
The retval function does nothing other than return a value's encapsulation. This package will indicate that it is a return value.

<?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);
}
In order to turn a coprocessor into a coprocessor (it supports a child invocation), we will have to write another function (obviously, it is another coprocessor):

&lt;?php





function Stackedcoroutine (generator $gen) {


$stack = new Splstack;





for (;;) {


$value = $gen-&gt;current ();





if ($value instanceof Generator) {


$stack-&gt;push ($gen);


$gen = $value;


Continue


}





$isReturnValue = $value instanceof coroutinereturnvalue;


if (! $gen-&gt;valid () | | $isReturnValue) {


if ($stack-&gt;isempty ()) {


Return


}





$gen = $stack-&gt;pop ();


$gen-&gt;send ($isReturnValue $value-&gt;getvalue (): NULL);


Continue


}





$gen-&gt;send (Yield $gen-&gt;key () =&gt; $value);


}


}


This function acts as a simple proxy between the caller and the currently running child. In $gen-&gt;send (yield $gen-&gt;key () =&gt; $value); This line completes the agent function. In addition it checks whether the return value is a generator, If it is a generator, it will start running the generator and push the previous coprocessor into the stack. Once it gets the coroutinereturnvalue, it will request the stack to pop again, and then proceed to the previous coprocessor.

In order for the process stack to be available in the task, the $this-coroutine = $coroutine in the task constructor; this line needs to be replaced by $this->coroutine = Stackedcoroutine ($coroutine);.

Now we can slightly improve on the Web server example above: Wait+read (and Wait+write and warit+accept) are grouped into functions. To group related functions, I will use the following classes:

&lt;?php





Class Cosocket {


protected $socket;





Public function __construct ($socket) {


$this-&gt;socket = $socket;


}





Public Function accept () {


Yield Waitforread ($this-&gt;socket);


Yield retval (New Cosocket (Stream_socket_accept ($this-&gt;socket, 0));


}





Public function Read ($size) {


Yield Waitforread ($this-&gt;socket);


Yield retval (fread ($this-&gt;socket, $size));


}





Public function Write ($string) {


Yield Waitforwrite ($this-&gt;socket);


Fwrite ($this-&gt;socket, $string);


}





Public function Close () {


@fclose ($this-&gt;socket);


}


}


Now the server can write a little bit more concise:

&lt;?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-&gt;accept ())


);


}


}





function Handleclient ($socket) {


$data = (yield $socket-&gt;read (8192));





$msg = "Received following request:\n\n$data";


$msgLength = strlen ($msg);





$response = &lt;&lt;&lt;res


http/1.1 ok\r


Content-type:text/plain\r


Content-length: $msgLength \ r


Connection:close\r


\ r


$msg


RES;





Yield $socket-&gt;write ($response);


Yield $socket-&gt;close ();


}


Error handling

As a good programmer, I believe you have noticed that the above example lacks error handling. Almost all sockets are error-prone. The reason why I did not do this is partly because of the boredom of error handling (especially the socket), but also because it easily expands the size of the code.

However, I still want to talk about common process error handling: The coprocessor allows you to throw an error inside it using the throw () method.

The throw () method takes a Exception and throws it to the current hanging point of the coprocessor to see the following code:

<?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"
That's very good, isn't there? Because we can now use the system call and the subroutine call exception to throw.

But we need to make some minor adjustments to the system call Scheduler::run () method:

<?php
if ($retval instanceof Systemcall) {
try {
$retval ($task, $this);
catch (Exception $e) {
$task->setexception ($e);
$this->schedule ($task);
}
Continue
}
The Task class also adds throw call handling:

&lt;?php


Class Task {


// ...


protected $exception = null;





Public Function SetException ($exception) {


$this-&gt;exception = $exception;


}





Public Function run () {


if ($this-&gt;beforefirstyield) {


$this-&gt;beforefirstyield = false;


return $this-&gt;coroutine-&gt;current ();


} elseif ($this-&gt;exception) {


$retval = $this-&gt;coroutine-&gt;throw ($this-&gt;exception);


$this-&gt;exception = null;


return $retval;


} else {


$retval = $this-&gt;coroutine-&gt;send ($this-&gt;sendvalue);


$this-&gt;sendvalue = null;


return $retval;


}


}





// ...


}


Now, we can already use exceptions thrown in system calls! For example, to invoke Killtask, let's throw an exception when the pass ID is not available:

<?php
Function Killtask ($tid) {
    return new Systemcall (
    & nbsp;   function (Task $task, Scheduler $scheduler) use ($tid) {
             if ($scheduler->killtask ($tid)) {
                 $scheduler->schedule ($task);
           } else {
                 throw new InvalidArgumentException (' Invalid task id! ');
           }
       }
   );
}
Try:

<?php
function Task () {
try {
Yield Killtask (500);
catch (Exception $e) {
Echo ' tried to kill task but failed: ', $e->getmessage (), "\ n";
}
}
The code is not functioning properly now because the Stackedcoroutine function does not handle the exception correctly. To fix it, you need to make adjustments:

&lt;?php


function Stackedcoroutine (generator $gen) {


$stack = new Splstack;


$exception = null;





for (;;) {


try {


if ($exception) {


$gen-&gt;throw ($exception);


$exception = null;


Continue


}





$value = $gen-&gt;current ();





if ($value instanceof Generator) {


$stack-&gt;push ($gen);


$gen = $value;


Continue


}





$isReturnValue = $value instanceof coroutinereturnvalue;


if (! $gen-&gt;valid () | | $isReturnValue) {


if ($stack-&gt;isempty ()) {


Return


}





$gen = $stack-&gt;pop ();


$gen-&gt;send ($isReturnValue $value-&gt;getvalue (): NULL);


Continue


}





try {


$sendValue = (yield $gen-&gt;key () =&gt; $value);


catch (Exception $e) {


$gen-&gt;throw ($e);


Continue


}





$gen-&gt;send ($sendValue);


catch (Exception $e) {


if ($stack-&gt;isempty ()) {


Throw $e;


}





$gen = $stack-&gt;pop ();


$exception = $e;


}


}


}


Conclusion

In this article, I used multitasking to build a task scheduler that included performing "system calls," non-blocking operations, and handling errors. The really cool thing about all of this is that the result code for the task looks completely synchronized, even when the task is performing a large number of asynchronous operations.

If you intend to read data from the socket, you will not need to pass a callback function or register an event listener. Instead, you just write yield $socket->read (). Most of this is what you will often write, adding yield to the front of it.

When I first heard of the coprocessor, I found the concept completely convincing, because it inspired me to implement it in PHP. At the same time I find that the process is really amazing: there is only one line between awesome code and a lot of code, and I think the coprocessor is just on this wire, not much more. However, it is a matter of opinion to say whether it is really beneficial to write asynchronous code using the method described above.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.