Home  >  Article  >  Backend Development  >  Detailed introduction to coroutines in php (code)

Detailed introduction to coroutines in php (code)

不言
不言Original
2018-09-13 16:57:283844browse

This article first introduces the concept of generator, focusing on the usage of yield and the interface of generator. The coroutine part briefly explains the principles of coroutines and matters that should be paid attention to in PHP coroutine programming.

PHP has introduced generator (Generator) since 5.5, based on which coroutine programming can be realized. This article starts with a review of generators and then transitions to coroutine programming.

yield and generator

Generator

Generator is a data type that implements the iterator interface. The generator instance cannot be obtained through new, and there is no static method to obtain the generator instance. The only way to get a generator instance is to call the generator function (a function containing the yield keyword). Calling the generator function directly returns a generator object, and the code within the function starts executing when the generator is running.

First go to the code to intuitively experience yield and generators:

# generator1.php
function foo() {
    exit('exit script when generator runs.');
    yield;
}

$gen = foo();
var_dump($gen);
$gen->current();

echo 'unreachable code!';

# 执行结果
object(Generator)#1 (0) {
}
exit script when generator runs.

fooThe function contains the yield keyword and transforms into a generator function. Calling foo does not execute any code in the function body, but instead returns a generator instance. After the generator runs, the code within the foo function is executed and the script ends.

As the name suggests, generators can be used to generate data. It’s just that the way it generates data is different from other functions: the generator returns data through yield instead of return; After yield returns data, the generator function does not It will be destroyed, it just pauses the operation, and it can be resumed from the pause in the future; the generator will return (only) one data if it is run once, and multiple data will be returned if it is run multiple times; if the generator is not called to obtain data, the code in the generator will lie there. The so-called "moving every time" refers to the way the generator generates data.

The generator implements the iterator interface. To obtain the generator data, you can use foreach loop or manual current/next/valid. The following code demonstrates data generation and traversal:

# generator2.php
function foo() {
  # 返回键值对数据
  yield "key1" => "value1";
  $count = 0;
  while ($count < 5) {
    # 返回值,key自动生成
    yield $count;
    ++ $count;
  }
  # 不返回值,相当于返回null
  yield;
}

# 手动获取生成器数据
$gen = foo();
while ($gen->valid()) {
  fwrite(STDOUT, "key:{$gen->key()}, value:{$gen->current()}\n");
  $gen->next();
}

# foreach 遍历数据
fwrite(STDOUT, "\ndata from foreach\n");
foreach (foo() as $key => $value) {
    fwrite(STDOUT, "key:$key, value:$value\n");
}

yield

yieldThe keyword is the core of the generator, which allows ordinary functions to differentiate (evolve) into generator functions. yield means "giving up". When the program executes to the yield statement, it will pause execution, yield the CPU and return control to the caller. The next execution will continue from the interruption point. implement. When control returns to the caller, the yield statement can return the value to the caller. generator2.phpThe script demonstrates the three forms of yield return values:

  1. yield $key => $value: Returns the key and value of the data;

  2. yield $value: Returns data, key is allocated by the system;

  3. yield: Returns null value, key is allocated by the system;

yieldAllows the function to pause, continue execution at any time, and return data to the caller. If external data is needed to continue execution, this work is provided by the send function of the generator: the variable that appears on the left side of yield will receive the variable from send value. Let’s look at a common send function usage example:

function logger(string $filename) {
  $fd = fopen($filename, 'w+');
  while($msg = yield) {
    fwrite($fd, date('Y-m-d H:i:s') . ':' . $msg . PHP_EOL);
  }
  fclose($fd);
}

$logger = logger('log.txt');
$logger->send('program starts!');
// do some thing
$logger->send('program ends!');

send The ability to have two-way data communication between generators and the outside: yieldReturn data; send Provide support data for continued operation. Since send allows the generator to continue executing, this behavior is similar to the next interface of the iterator, where next is equivalent to send(null).

Others

  1. $string = yield $data; expression is not legal before PHP7 and requires parentheses: $string = (yield $data);

  2. ##PHP5 generator function cannot

    return value, after PHP7 it can return value and pass the of the generator getReturnGet the returned value.

  3. PHP7 adds the

    yield from syntax to implement generator delegation.

  4. The generator is a one-way iterator, and

    rewind cannot be called after it is started.

Summary

Compared with other iterators, generators have the characteristics of low performance overhead and easy coding. Its role is mainly reflected in three aspects:

  1. Data generation (producer), returning data through yield;

  2. Data consumption (consumer) ), consume the data from send;

  3. implements the coroutine.

Regarding generators and basic usage in PHP, it is recommended to read the blog post of

2gua: PHP Generator is lively, interesting and easy to understand.

Coroutine programming

Coroutine (coroutine) is a subroutine that can be interrupted and resumed at any time. The

yield keyword allows the function to have this ability, so it can be used For coroutine programming.

Processes, threads and coroutines

Threads belong to processes, and a process can have multiple threads. A process is the smallest unit for computer allocation of resources, and a thread is the smallest unit for computer scheduling and execution. Both processes and threads are scheduled by the operating system.

Coroutines can be regarded as "user-mode threads" and require user programs to implement scheduling. Threads and processes are scheduled by the operating system to run alternately in a "preemptive" manner, and coroutines actively give up the CPU to run alternately in a "negotiative" manner. Coroutines are very lightweight, coroutine switching does not involve thread switching, and the execution efficiency is high. The greater the number, the better the advantages of coroutines can be reflected.

Generator and coroutine

The coroutine implemented by the generator is a stackless coroutine, that is, the generator function only has a function frame, which is attached to the caller's stack during runtime for execution. Unlike the powerful stackful coroutine, the generator cannot control the direction of the program after it is paused, and can only passively return control to the caller; the generator can only interrupt itself, not the entire coroutine. Of course, the advantage of the generator is that it is highly efficient (you only need to save the program counter when pausing) and is simple to implement.

Coroutine Programming

Speaking of coroutine programming in PHP, I believe most people have read this blog post reprinted (translated) by Brother Niao: Using coroutines to achieve multiple functions in PHP Task scheduling. The original author nikic is the core developer of PHP, the initiator and implementer of the generator function. If you want to learn more about generators and coroutine programming based on them, nikic's RFC on generators and the articles on Niaoge's website are must-reads.

Let’s first look at the working method of coroutines based on generators: coroutines work collaboratively, that is, coroutines actively give up the CPU to achieve alternate running of multiple tasks (that is, concurrent multitasking, but not parallel); A generator can be regarded as a coroutine. When the yield statement is executed, the CPU control is returned to the caller, and the caller continues to execute other coroutines or other codes.

Let’s look at the difficulty in understanding Brother Bird’s blog. Coroutines are very lightweight, and thousands of coroutines (generators) can exist in a system at the same time. The operating system does not schedule coroutines, and the work of arranging coroutine execution falls on developers. Some people don’t understand the coroutine part of Brother Niao’s article because it says there is little coroutine programming (writing coroutines mainly means writing generator functions), but they spend a lot of time implementing a coroutine scheduler (scheduler or kernel). : Simulates the operating system and performs fair scheduling on all coroutines. The general thinking of PHP development is: I wrote these codes, and the PHP engine will call my codes to get the expected results. Coroutine programming requires not only writing code to do the work, but also writing code to instruct these codes when to work. If you don’t have a good grasp of the author’s thinking, it will naturally be more difficult to understand. It needs to be scheduled by itself, which is a disadvantage of the generator coroutine compared to the native coroutine (async/await form).

Now that we know what coroutine is, what can it be used for? The coroutine gives up the CPU on its own to cooperate and efficiently utilize the CPU. Of course, the time to give up should be when the program is blocked. Where will the program block? User-mode code rarely blocks, and blocking is mainly caused by system calls. The majority of system calls are IO, so the main application scenario of coroutines is network programming. In order to make the program high performance and high concurrency, the program should execute asynchronously and not block. Since asynchronous execution requires notifications and callbacks, writing callback functions cannot avoid the problem of "callback hell": code readability is poor, and the program execution process is scattered among layers of callback functions. There are two main ways to solve callback hell: Promise and coroutines. Coroutines can write code in a synchronous manner and are recommended in high-performance network programming (IO-intensive).

Let’s look back at coroutine programming in PHP. In PHP, coroutine programming is implemented based on generators. It is recommended to use coroutine frameworks such as RecoilPHP and Amp. These frameworks have already written schedulers. If you develop a generator function directly on it, the kernel will automatically schedule execution (if you want a function to be scheduled for execution in a coroutine mode, just add yield to the function body. ). If you don’t want to use the yield method for coroutine programming, we recommend swoole or its derivative framework, which can achieve a coroutine programming experience similar to golang and enjoy the development efficiency of PHP.

If you want to use the original PHP coroutine programming, a scheduler similar to the one in Niao Ge's blog is essential. The scheduler schedules the execution of the coroutine. After the coroutine is interrupted, control returns to the scheduler. Therefore, the scheduler should always be in the main (event) loop, that is, when the CPU is not executing the coroutine, it should be executing the scheduler code. When running without a coroutine, the scheduler should block itself to avoid consuming the CPU (Niao Ge's blog uses the built-in select system call), wait for the event to arrive, and then execute the corresponding coroutine. During the running of the program, except for scheduler blocking, the coroutine should not call blocking APIs during running.

Summary

In coroutine programming, the main function of yield is to transfer control without worrying about its return value (basically yieldThe returned value will be send directly during the next execution). The focus should be on the timing of the transfer of control and how the coroutine operates.

In addition, it needs to be explained that coroutines have little to do with asynchrony, and it also depends on the running environment support. In the conventional PHP operating environment, even if promise/coroutine is used, it is still blocked synchronously. No matter how awesome the coroutine framework is, sleep is no longer easy to use. As an analogy, even if JavaScript does not use promise/async technologies, it is asynchronous and non-blocking.

Through generators and Promise, coroutine programming similar to await can be implemented. There is a lot of relevant code on Github and will not be given in this article.

Related recommendations:

$_SERVER in PHP Detailed introduction

Detailed introduction to output_buffering in PHP, outputbuffering_PHP tutorial

The above is the detailed content of Detailed introduction to coroutines in php (code). For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn