Maison > développement back-end > tutoriel php > Explication détaillée de l'implémentation de la synergie PHP (avec code)

Explication détaillée de l'implémentation de la synergie PHP (avec code)

不言
Libérer: 2023-04-04 10:32:02
avant
3133 Les gens l'ont consulté

Ce que cet article vous apporte est une explication détaillée de la mise en œuvre de la synergie PHP (avec du code). Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer.

Contenu de base que vous devez connaître pour implémenter les coroutines PHP.

Processus/threads multiples

Les premiers programmes côté serveur ont résolu le problème des E/S simultanées grâce à plusieurs processus et multi-threads. Le modèle de processus est apparu pour la première fois sous Unix La notion de processus existe depuis la naissance du système. Les premiers programmes côté serveur sont généralement Accepter Une connexion client crée un processus, puis le processus enfant entre dans une boucle pour interagir avec la connexion client de manière synchrone et bloquante, en envoyant et en recevant des données de traitement.

Le mode multi-threading est apparu plus tard. Les threads sont plus légers que les processus, et les piles de mémoire sont partagées entre les threads, donc l'interaction entre les différents threads est très facile à mettre en œuvre. Par exemple, si vous implémentez une salle de discussion, les connexions des clients peuvent interagir les unes avec les autres et les joueurs de la salle de discussion peuvent envoyer des messages à n'importe quelle autre personne. C'est très simple à mettre en œuvre en mode multi-thread. Les données peuvent être envoyées directement à une connexion client dans le thread. Le mode multi-processus nécessite l'utilisation de pipelines, de files d'attente de messages, de mémoire partagée et d'autres technologies complexes collectivement appelées communication inter-processus (IPC) pour y parvenir.

Le modèle de serveur multi-processus le plus simple

$serv = stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr)
or die("Create server failed");
while(1) {	
$conn = stream_socket_accept($serv);	
if (pcntl_fork() == 0) {	
	$request = fread($conn);		
	// do something		
	// $response = "hello world";		
	fwrite($response);		
	fclose($conn);		
	exit(0);	
	}
}
Copier après la connexion

Le processus du modèle multi-processus/thread est :

Créez un socket, liez le port du serveur (bind) et écoutez le port (écouter). En PHP, vous pouvez utiliser la fonction stream_socket_server pour effectuer les trois étapes ci-dessus. Bien sûr, vous pouvez également utiliser des sockets de niveau inférieur. extensions pour les implémenter séparément.

Entrez dans la boucle while, bloquez l'opération d'acceptation et attendez que la connexion client arrive. À ce moment-là, le programme entrera en état de veille jusqu'à ce qu'un nouveau client initie une connexion au serveur, et le système d'exploitation réveillera le processus. La fonction accept renvoie le socket connecté par le client. Le processus principal crée un processus enfant via fork (php : pcntl_fork) sous le modèle multi-processus, et utilise pthread_create (php : new Thread) sous le modèle multi-thread pour créer un fil enfant.

S'il n'y a pas de déclaration spéciale ci-dessous, le processus sera utilisé pour représenter à la fois le processus/le thread.

Une fois la création réussie du processus enfant, il entre dans la boucle while, bloquant l'appel recv (php:fread), en attendant que le client envoie des données au serveur. Après avoir reçu les données, le programme serveur les traite puis utilise send (php : fwrite) pour envoyer une réponse au client. Un service à connexion longue continuera à interagir avec le client, tandis qu'un service à connexion courte se fermera généralement après avoir reçu une réponse.

Lorsque la connexion client est fermée, le processus enfant se ferme et détruit toutes les ressources, et le processus principal recyclera le processus enfant.

Explication détaillée de limplémentation de la synergie PHP (avec code)

Le plus gros problème de ce modèle est que les frais généraux de création et de destruction des processus sont très élevés. Le modèle ci-dessus ne peut donc pas être appliqué à des programmes serveur très chargés. La version améliorée correspondante résout ce problème, qui est le modèle classique Leader-Follower.

$serv = stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr)
or die("Create server failed");
for($i = 0; $i < 32; $i++) {
if (pcntl_fork() == 0)
 {
 while(1) {
 $conn = stream_socket_accept($serv);
 if ($conn == false) continue;
 // do something
 $request = fread($conn);
 // $response = "hello world";
 fwrite($response);
 fclose($conn);
 }
 exit(0);
 }
 }
Copier après la connexion

Sa caractéristique est que N processus seront créés après le démarrage du programme. Chaque processus enfant entre dans Accept et attend l'arrivée de nouvelles connexions. Lorsqu'un client se connecte au serveur, l'un des processus enfants est réveillé, commence à traiter les demandes du client et n'accepte plus de nouvelles connexions TCP. Lorsque cette connexion est fermée, le processus enfant sera libéré, entrera à nouveau dans Accept et participera au traitement des nouvelles connexions.

L'avantage de ce modèle est qu'il permet de réutiliser totalement le procédé, sans consommation supplémentaire et avec un très bon rendement. De nombreux programmes serveur courants sont basés sur ce modèle, comme Apache et PHP-FPM.

Le modèle multi-processus présente également certains inconvénients.

Ce modèle s'appuie fortement sur le nombre de processus pour résoudre les problèmes de concurrence. Une connexion client nécessite un processus. Le nombre de processus de travail dépend de la capacité de traitement simultané. Le système d'exploitation est limité dans le nombre de processus qu'il peut créer.

Le démarrage d'un grand nombre de processus entraînera une consommation supplémentaire de planification de processus. Lorsqu'il y a des centaines de processus, le coût de planification du changement de contexte de processus peut être négligeable, représentant moins de 1 % du processeur. Si des milliers, voire des dizaines de milliers de processus sont démarrés, la consommation montera en flèche. La consommation de planification peut représenter des dizaines, voire 100 % du CPU.

Parallèle et concurrence

Lorsque nous parlons de modèles multi-processus et similaires qui effectuent plusieurs tâches en même temps, nous devons d'abord parler de parallélisme et de concurrence.

Concurrence

fait référence à la capacité de gérer plusieurs activités simultanées ne doivent pas nécessairement se produire en même temps.

Parallesim

fait référence à deux événements simultanés qui se produisent en même temps, ce qui a le sens de concurrence, mais la concurrence n'est pas nécessairement parallèle.

La différence

"Concurrency" fait référence à la structure du programme, "Parallèle" fait référence à l'état du programme lorsqu'il est en cours d'exécution

"Parallèle" est certain qu'il est concurrent, et "parallèle" est un type de conception "concurrente"

Un seul thread ne peut jamais atteindre l'état "parallèle"

La norme pour une concurrence correcte la conception est la suivante :

Permettre à plusieurs opérations d'avoir lieu pendant des périodes qui se chevauchent.
deux tâches peuvent démarrer, s'exécuter et se terminer dans des périodes qui se chevauchent

Itérateur et générateur

在了解 PHP 协程前,还有 迭代器 和 生成器 这两个概念需要先认识一下。

迭代器

PHP5 开始内置了 Iterator 即迭代器接口,所以如果你定义了一个类,并实现了Iterator 接口,那么你的这个类对象就是 ZEND_ITER_OBJECT 即可迭代的,否则就是 ZEND_ITER_PLAIN_OBJECT。

对于 ZEND_ITER_PLAIN_OBJECT 的类,foreach 会获取该对象的默认属性数组,然后对该数组进行迭代。

而对于 ZEND_ITER_OBJECT 的类对象,则会通过调用对象实现的 Iterator 接口相关函数来进行迭代。

任何实现了 Iterator 接口的类都是可迭代的,即都可以用 foreach 语句来遍历。

Iterator 接口

interface Iterator extends Traversable
{	
// 获取当前内部标量指向的元素的数据
public mixed current()	
// 获取当前标量
public scalar key()	
// 移动到下一个标量
public void next()	
// 重置标量
public void rewind()	
// 检查当前标量是否有效
public boolean valid()
}
Copier après la connexion

常规实现 range 函数

PHP 自带的 range 函数原型:

range — 根据范围创建数组,包含指定的元素

array range (mixed $start , mixed $end [, number $step = 1 ])

建立一个包含指定范围单元的数组。

在不使用迭代器的情况要实现一个和 PHP 自带的 range 函数类似的功能,可能会这么写:

function range ($start, $end, $step = 1){
$ret = [];
for ($i = $start; $i <= $end; $i += $step) {
$ret[] = $i;
}
return $ret;
}
Copier après la connexion

需要将生成的所有元素放在内存数组中,如果需要生成一个非常大的集合,则会占用巨大的内存。

迭代器实现 xrange 函数

来看看迭代实现的 range,我们叫做 xrange,他实现了 Iterator 接口必须的 5 个方法:

class Xrange implements Iterator
{
protected $start;
protected $limit;
protected $step;
protected $current;
public function __construct($start, $limit, $step = 1){
$this->start = $start;
$this->limit = $limit;
$this->step  = $step;
}
public function rewind(){
$this->current = $this->start;
}
public function next(){
$this->current += $this->step;
}
public function current(){
return $this->current;
}
public function key(){
return $this->current + 1;
}
public function valid(){
return $this->current <= $this->limit;
}
}
Copier après la connexion

使用时代码如下:

foreach (new Xrange(0, 9) as $key => $val)
 {
   echo $key, &#39; &#39;, $val, "\n";
 }
Copier après la connexion


输出:

0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
Copier après la connexion

看上去功能和 range() 函数所做的一致,不同点在于迭代的是一个 对象(Object) 而不是数组:

var_dump(new Xrange(0, 9));
Copier après la connexion

输出:

object(Xrange)#1 (4) {
["start":protected]=>
int(0)
["limit":protected]=>
int(9)
["step":protected]=>
int(1)
["current":protected]=>NULL
}
Copier après la connexion

另外,内存的占用情况也完全不同:

// range
$startMemory = memory_get_usage();
$arr = range(0, 500000);
echo &#39;range(): &#39;, memory_get_usage() - $startMemory, " bytes\n";
unset($arr);
// xrange
$startMemory = memory_get_usage();
$arr = new Xrange(0, 500000);
echo &#39;xrange(): &#39;, memory_get_usage() - $startMemory, " bytes\n";
Copier après la connexion

输出:

xrange(): 624 bytes
range(): 72194784 bytes
Copier après la connexion

range() 函数在执行后占用了 50W 个元素内存空间,而 xrange 对象在整个迭代过程中只占用一个对象的内存。

Yii2 Query

在喜闻乐见的各种 PHP 框架里有不少生成器的实例,比如 Yii2 中用来构建 SQL 语句的 \yii\db\Query 类:

$query = (new \yii\db\Query)->from(&#39;user&#39;);
// yii\db\BatchQueryResult
foreach ($query->batch() as $users) {
// 每次循环得到多条 user 记录
}
Copier après la connexion

来看一下 batch() 做了什么:

/**
* Starts a batch query.
*
* A batch query supports fetching data in batches, which can keep the memory usage under a limit.
* This method will return a [[BatchQueryResult]] object which implements the [[\Iterator]] interface
* and can be traversed to retrieve the data in batches.
*
* For example,
*
*
* $query = (new Query)->from(&#39;user&#39;);
* foreach ($query->batch() as $rows) {
*     // $rows is an array of 10 or fewer rows from user table
* }
*
*
* @param integer $batchSize the number of records to be fetched in each batch.
* @param Connection $db the database connection. If not set, the "db" application component will be used.
* @return BatchQueryResult the batch query result. It implements the [[\Iterator]] interface
* and can be traversed to retrieve the data in batches.
*/
public function batch($batchSize = 100, $db = null){
return Yii::createObject([
&#39;class&#39; => BatchQueryResult::className(),
&#39;query&#39; => $this,
&#39;batchSize&#39; => $batchSize,
&#39;db&#39; => $db,
&#39;each&#39; => false,
]);
}
Copier après la connexion

实际上返回了一个 BatchQueryResult 类,类的源码实现了 Iterator 接口 5 个关键方法:

class BatchQueryResult extends Object implements \Iterator
{
public $db;
public $query;
public $batchSize = 100;
public $each = false;
private $_dataReader;
private $_batch;
private $_value;
private $_key;
/**
* Destructor.
*/
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}
/**
* Resets the batch query.
* This method will clean up the existing batch query so that a new batch query can be performed.
*/
public function reset(){
if ($this->_dataReader !== null)
{
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
}
/*
*
* Resets the iterator to the initial state.
* This method is required by the interface [[\Iterator]].
*/
public function rewind()
{
$this->reset();
$this->next();
}
/**
* Moves the internal pointer to the next dataset.
* This method is required by the interface [[\Iterator]].
*/
public function next()
{
if ($this->_batch === null || !$this->each || $this->each && next($this->_batch) === false) 
{
$this->_batch = $this->fetchData();
reset($this->_batch);
}
if ($this->each) 
{
$this->_value = current($this->_batch);
if ($this->query->indexBy !== null) 
{
$this->_key = key($this->_batch);
} 
elseif (key($this->_batch) !== null) 
{
$this->_key++;
} else {
$this->_key = null;
}
} else {
$this->_value = $this->_batch;
$this->_key = $this->_key === null ? 0 : $this->_key + 1;
}
}
/**
* Fetches the next batch of data.
* @return array the data fetched
*/
protected function fetchData()
{
// ...
}
/**
* Returns the index of the current dataset.
* This method is required by the interface [[\Iterator]].
* @return integer the index of the current row.
*/
public function key()
{
return $this->_key;
}
/**
* Returns the current dataset.
* This method is required by the interface [[\Iterator]].
* @return mixed the current dataset.
*/
public function current()
{
return $this->_value;
}
/**
* Returns whether there is a valid dataset at the current position.
* This method is required by the interface [[\Iterator]].
* @return boolean whether there is a valid dataset at the current position.
*/
public function valid()
{
return !empty($this->_batch);
}
}
Copier après la connexion

以迭代器的方式实现了类似分页取的效果,同时避免了一次性取出所有数据占用太多的内存空间。

迭代器使用场景

使用返回迭代器的包或库时(如 PHP5 中的 SPL 迭代器)

无法在一次调用获取所需的所有元素时

要处理数量巨大的元素时(数据库中要处理的结果集内容超过内存)

生成器

需要 PHP 5 >= 5.5.0 或 PHP 7

虽然迭代器仅需继承接口即可实现,但毕竟需要定义一整个类然后实现接口的所有方法,实在是不怎么方便。

生成器则提供了一种更简单的方式来实现简单的对象迭代,相比定义类来实现 Iterator 接口的方式,性能开销和复杂度大大降低。

PHP Manual

生成器允许在 foreach 代码块中迭代一组数据而不需要创建任何数组。一个生成器函数,就像一个普通的有返回值的自定义函数类似,但普通函数只返回一次, 而生成器可以根据需要通过 yield 关键字返回多次,以便连续生成需要迭代返回的值。

一个最简单的例子就是使用生成器来重新实现 xrange() 函数。效果和上面我们用迭代器实现的差不多,但实现起来要简单的多。

生成器实现 xrange 函数

function xrange($start, $limit, $step = 1) 
{
for ($i = 0; $i < $limit; $i += $step) {
yield $i + 1 => $i;
}
}
foreach (xrange(0, 9) as $key => $val) {
printf("%d %d \n", $key, $val);
}
// 输出
// 1 0
// 2 1
// 3 2
// 4 3
// 5 4
// 6 5
// 7 6
// 8 7
// 9 8
Copier après la connexion

实际上生成器生成的正是一个迭代器对象实例,该迭代器对象继承了 Iterator 接口,同时也包含了生成器对象自有的接口,具体可以参考 Generator 类的定义以及语法参考。

同时需要注意的是:

一个生成器不可以返回值,这样做会产生一个编译错误。然而 return 空是一个有效的语法并且它将会终止生成器继续执行。

yield 关键字

需要注意的是 yield 关键字,这是生成器的关键。通过上面的例子可以看出,yield 会将当前产生的值传递给 foreach,换句话说,foreach 每一次迭代过程都会从 yield 处取一个值,直到整个遍历过程不再能执行到 yield 时遍历结束,此时生成器函数简单的退出,而调用生成器的上层代码还可以继续执行,就像一个数组已经被遍历完了。

yield 最简单的调用形式看起来像一个 return 申明,不同的是 yield 暂停当前过程的执行并返回值,而 return 是中断当前过程并返回值。暂停当前过程,意味着将处理权转交由上一级继续进行,直到上一级再次调用被暂停的过程,该过程又会从上一次暂停的位置继续执行。这像是什么呢?如果之前已经在鸟哥的文章中粗略看过,应该知道这很像操作系统的进程调度,多个进程在一个 CPU 核心上执行,在系统调度下每一个进程执行一段指令就被暂停,切换到下一个进程,这样外部用户看起来就像是同时在执行多个任务。

但仅仅如此还不够,yield 除了可以返回值以外,还能接收值,也就是可以在两个层级间实现双向通信。

来看看如何传递一个值给 yield:

function printer()
{
while (true) 
{
printf("receive: %s\n", yield);
}
}
$printer = printer();
$printer->send(&#39;hello&#39;);
$printer->send(&#39;world&#39;);
// 输出
receive: hello
receive: world
Copier après la connexion

根据 PHP 官方文档的描述可以知道 Generator 对象除了实现 Iterator 接口中的必要方法以外,还有一个 send 方法,这个方法就是向 yield 语句处传递一个值,同时从 yield 语句处继续执行,直至再次遇到 yield 后控制权回到外部。

既然 yield 可以在其位置中断并返回或者接收一个值,那能不能同时进行接收和返回呢?当然,这也是实现协程的根本。对上述代码做出修改:

function printer()
{
$i = 0;
while (true) {
printf("receive: %s\n", (yield ++$i));
}
}
$printer = printer();
printf("%d\n", $printer->current());
$printer->send(&#39;hello&#39;);
printf("%d\n", $printer->current());
$printer->send(&#39;world&#39;);
printf("%d\n", $printer->current());
// 输出
1
receive: hello
2
receive: world
3
Copier après la connexion

这是另一个例子:

function gen() 
{
$ret = (yield &#39;yield1&#39;);
var_dump($ret);
$ret = (yield &#39;yield2&#39;);
var_dump($ret);
}
$gen = gen();
var_dump($gen->current());   // string(6) "yield1"
var_dump($gen->send(&#39;ret1&#39;)); // string(4) "ret1"   (第一个 var_dump)
                              // string(6) "yield2" (继续执行到第二个 yield,吐出了返回值)
var_dump($gen->send(&#39;ret2&#39;)); // string(4) "ret2"   (第二个 var_dump)
                              // NULL (var_dump 之后没有其他语句,所以这次 ->send() 的返回值为 null)
Copier après la connexion

current 方法是迭代器 Iterator 接口必要的方法,foreach 语句每一次迭代都会通过其获取当前值,而后调用迭代器的 next 方法。在上述例子里则是手动调用了 current 方法获取值。

上述例子已经足以表示 yield 能够作为实现双向通信的工具,也就是具备了后续实现协程的基本条件。

上面的例子如果第一次接触并稍加思考,不免会疑惑为什么一个 yield 既是语句又是表达式,而且这两种情况还同时存在:

对于所有在生成器函数中出现的 yield,首先它都是语句,而跟在 yield 后面的任何表达式的值将作为调用生成器函数的返回值,如果 yield 后面没有任何表达式(变量、常量都是表达式),那么它会返回 NULL,这一点和 return 语句一致。

yield 也是表达式,它的值就是 send 函数传过来的值(相当于一个特殊变量,只不过赋值是通过 send 函数进行的)。只要调用send方法,并且生成器对象的迭代并未终结,那么当前位置的 yield 就会得到 send 方法传递过来的值,这和生成器函数有没有把这个值赋值给某个变量没有任何关系。

这个地方可能需要仔细品味上面两个 send() 方法的例子才能理解。但可以简单的记住:

任何时候 yield 关键词即是语句:可以为生成器函数返回值;也是表达式:可以接收生成器对象发过来的值。

除了 send() 方法,还有一种控制生成器执行的方法是 next() 函数:

Next(),恢复生成器函数的执行直到下一个 yield

Send(),向生成器传入一个值,恢复执行直到下一个 yield

协程

对于单核处理器,多进程实现多任务的原理是让操作系统给一个任务每次分配一定的 CPU 时间片,然后中断、让下一个任务执行一定的时间片接着再中断并继续执行下一个,如此反复。由于切换执行任务的速度非常快,给外部用户的感受就是多个任务的执行是同时进行的。

多进程的调度是由操作系统来实现的,进程自身不能控制自己何时被调度,也就是说:

进程的调度是由外层调度器抢占式实现的

而协程要求当前正在运行的任务自动把控制权回传给调度器,这样就可以继续运行其他任务。这与『抢占式』的多任务正好相反, 抢占多任务的调度器可以强制中断正在运行的任务, 不管它自己有没有意愿。『协作式多任务』在 Windows 的早期版本 (windows95) 和 Mac OS 中有使用, 不过它们后来都切换到『抢占式多任务』了。理由相当明确:如果仅依靠程序自动交出控制的话,那么一些恶意程序将会很容易占用全部 CPU 时间而不与其他任务共享。

协程的调度是由协程自身主动让出控制权到外层调度器实现的

回到刚才生成器实现 xrange 函数的例子,整个执行过程的交替可以用下图来表示:

Explication détaillée de limplémentation de la synergie PHP (avec code)

协程可以理解为纯用户态的线程,通过协作而不是抢占来进行任务切换。相对于进程或者线程,协程所有的操作都可以在用户态而非操作系统内核态完成,创建和切换的消耗非常低。

简单的说 Coroutine(协程) 就是提供一种方法来中断当前任务的执行,保存当前的局部变量,下次再过来又可以恢复当前局部变量继续执行。

我们可以把大任务拆分成多个小任务轮流执行,如果有某个小任务在等待系统 IO,就跳过它,执行下一个小任务,这样往复调度,实现了 IO 操作和 CPU 计算的并行执行,总体上就提升了任务的执行效率,这也便是协程的意义。

PHP 协程和 yield

PHP 从 5.5 开始支持生成器及 yield 关键字,而 PHP 协程则由 yield 来实现。

要理解协程,首先要理解:代码是代码,函数是函数。函数包裹的代码赋予了这段代码附加的意义:不管是否显式的指明返回值,当函数内的代码块执行完后都会返回到调用层。而当调用层调用某个函数的时候,必须等这个函数返回,当前函数才能继续执行,这就构成了后进先出,也就是 Stack。

而协程包裹的代码,不是函数,不完全遵守函数的附加意义,协程执行到某个点,协会协程会 yield 返回一个值然后挂起,而不是 return 一个值然后结束,当再次调用协程的时候,会在上次 yield 的点继续执行。

所以协程违背了通常操作系统和 x86 的 CPU 认定的代码执行方式,也就是 Stack 的这种执行方式,需要运行环境(比如 php,python 的 yield 和 golang 的 goroutine)自己调度,来实现任务的中断和恢复,具体到 PHP,就是靠 yield 来实现。

堆栈式调用 和 协程调用的对比:

Explication détaillée de limplémentation de la synergie PHP (avec code)

结合之前的例子,可以总结一下 yield 能做的就是:

实现不同任务间的主动让位、让行,把控制权交回给任务调度器。

通过 send() 实现不同任务间的双向通信,也就可以实现任务和调度器之间的通信。

yield 就是 PHP 实现协程的方式。

协程多任务调度

下面是雄文 Cooperative multitasking using coroutines (in PHP!) 里一个简单但完整的例子,来展示如何具体的在 PHP 里实现协程任务的调度。

首先是一个任务类:

Task

class Task
{
// 任务 IDprotected 
$taskId;
// 协程对象protected 
$coroutine;
// send() 值protected 
$sendVal = null;
// 是否首次 yieldprotected 
$beforeFirstYield = true;
public function __construct($taskId, Generator $coroutine) {
$this->taskId = $taskId;
$this->coroutine = $coroutine;
}
public function getTaskId() {
return $this->taskId;
}
public function setSendValue($sendVal) {
$this->sendVal = $sendVal;
}
public function run() {
// 如之前提到的在send之前, 当迭代器被创建后第一次 yield 之前,一个 renwind() 方法会被隐式调用
// 所以实际上发生的应该类似:
// $this->coroutine->rewind();
// $this->coroutine->send();
// 这样 renwind 的执行将会导致第一个 yield 被执行, 并且忽略了他的返回值.
// 真正当我们调用 yield 的时候, 我们得到的是第二个yield的值,导致第一个yield的值被忽略。
// 所以这个加上一个是否第一次 yield 的判断来避免这个问题
if ($this->beforeFirstYield) {
$this->beforeFirstYield = false;
return $this->coroutine->current();
} else {
$retval = $this->coroutine->send($this->sendVal);
$this->sendVal = null;
return $retval;
}
}
public function isFinished() {
return !$this->coroutine->valid();
}
}
Copier après la connexion

接下来是调度器,比 foreach 是要复杂一点,但好歹也能算个正儿八经的 Scheduler :)

Scheduler

class Scheduler
{
protected $maxTaskId = 0;
protected $taskMap = []; // taskId => task
protected $taskQueue;
public function __construct() {
$this->taskQueue = new SplQueue();
}
// (使用下一个空闲的任务id)创建一个新任务,然后把这个任务放入任务map数组里. 接着它通过把任务放入任务队列里来实现对任务的调度. 接着run()方法扫描任务队列, 运行任务.如果一个任务结束了, 那么它将从队列里删除, 否则它将在队列的末尾再次被调度。
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->queue->enqueue($task);
}
public function run() {
while (!$this->queue->isEmpty()) {        
// 任务出队
$task = $this->queue->dequeue();
$task->run();
if ($task->isFinished()) {
unset($this->taskMap[$task->getTaskId()]);
} else {
$this->schedule($task);
}
}
}
}
Copier après la connexion

队列可以使每个任务获得同等的 CPU 使用时间,

Demo

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();
Copier après la connexion

输出:

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.
Copier après la connexion

结果正是我们期待的,最初的 5 次迭代,两个任务是交替进行的,而在第二个任务结束后,只有第一个任务继续执行到结束。

协程非阻塞 IO

若想真正的发挥出协程的作用,那一定是在一些涉及到阻塞 IO 的场景,我们都知道 Web 服务器最耗时的部分通常都是 socket 读取数据等操作上,如果进程对每个请求都挂起的等待 IO 操作,那处理效率就太低了,接下来我们看个支持非阻塞 IO 的 Scheduler:

<?php
class Scheduler
{
protected $maxTaskId = 0;
protected $tasks = []; // taskId => task
protected $queue;
// resourceID => [socket, tasks]
protected $waitingForRead = [];
protected $waitingForWrite = [];
public function __construct() {
// SPL 队列
$this->queue = new SplQueue();
}
public function newTask(Generator $coroutine) {
$tid = ++$this->maxTaskId;
$task = new Task($tid, $coroutine);
$this->tasks[$tid] = $task;
$this->schedule($task);
return $tid;
}
public function schedule(Task $task) {    
// 任务入队
$this->queue->enqueue($task);
}
public function run() {
while (!$this->queue->isEmpty()) {        
// 任务出队
$task = $this->queue->dequeue();
$task->run();
if ($task->isFinished()) {
unset($this->tasks[$task->getTaskId()]);
} else {
$this->schedule($task);
}
}
}
public function waitForRead($socket, Task $task)
{
if (isset($this->waitingForRead[(int)$socket])) {
$this->waitingForRead[(int)$socket][1][] = $task;
} else {
$this->waitingForRead[(int)$socket] = [$socket, [$task]];
}
}
public function waitForWrite($socket, Task $task)
{
if (isset($this->waitingForWrite[(int)$socket])) {
$this->waitingForWrite[(int)$socket][1][] = $task;
} else {
$this->waitingForWrite[(int)$socket] = [$socket, [$task]];
}
}
/**
* @param $timeout 0 represent
*/
protected function ioPoll($timeout)
{
$rSocks = [];
foreach ($this->waitingForRead as list($socket)) {
$rSocks[] = $socket;
}
$wSocks = [];
foreach ($this->waitingForWrite as list($socket)) {
$wSocks[] = $socket;
}
$eSocks = [];
// $timeout 为 0 时, stream_select 为立即返回,为 null 时则会阻塞的等,见 http://php.net/manual/zh/function.stream-select.php
if (!@stream_select($rSocks, $wSocks, $eSocks, $timeout)) {
return;
}
foreach ($rSocks as $socket) {
list(, $tasks) = $this->waitingForRead[(int)$socket];
unset($this->waitingForRead[(int)$socket]);
foreach ($tasks as $task) {
$this->schedule($task);
}
}
foreach ($wSocks as $socket) {
list(, $tasks) = $this->waitingForWrite[(int)$socket];
unset($this->waitingForWrite[(int)$socket]);
foreach ($tasks as $task) {
$this->schedule($task);
}
}
}
/**
* 检查队列是否为空,若为空则挂起的执行 stream_select,否则检查完 IO 状态立即返回,详见 ioPoll()
* 作为任务加入队列后,由于 while true,会被一直重复的加入任务队列,实现每次任务前检查 IO 状态
* @return Generator object for newTask
*
*/
protected function ioPollTask()
{
while (true) {
if ($this->taskQueue->isEmpty()) {
$this->ioPoll(null);
} else {
$this->ioPoll(0);
}
yield;
}
}
/**
* $scheduler = new Scheduler;
* $scheduler->newTask(Web Server Generator);
* $scheduler->withIoPoll()->run();
*
* 新建 Web Server 任务后先执行 withIoPoll() 将 ioPollTask() 作为任务入队
*
* @return $this
*/
public function withIoPoll()
{
$this->newTask($this->ioPollTask());
return $this;
}
}
Copier après la connexion

这个版本的 Scheduler 里加入一个永不退出的任务,并且通过 stream_select 支持的特性来实现快速的来回检查各个任务的 IO 状态,只有 IO 完成的任务才会继续执行,而 IO 还未完成的任务则会跳过,完整的代码和例子可以戳这里。

也就是说任务交替执行的过程中,一旦遇到需要 IO 的部分,调度器就会把 CPU 时间分配给不需要 IO 的任务,等到当前任务遇到 IO 或者之前的任务 IO 结束才再次调度 CPU 时间,以此实现 CPU 和 IO 并行来提升执行效率,类似下图:

Explication détaillée de limplémentation de la synergie PHP (avec code)

单任务改造

如果想将一个单进程任务改造成并发执行,我们可以选择改造成多进程或者协程:

多进程,不改变任务执行的整体过程,在一个时间段内同时执行多个相同的代码段,调度权在 CPU,如果一个任务能独占一个 CPU 则可以实现并行。

协程,把原有任务拆分成多个小任务,原有任务的执行流程被改变,调度权在进程自己,如果有 IO 并且可以实现异步,则可以实现并行。

多进程改造

Explication détaillée de limplémentation de la synergie PHP (avec code)

协程改造

Explication détaillée de limplémentation de la synergie PHP (avec code)

Coroutines et Go Goroutines

Coroutines en PHP ou dans d'autres langages, tels que Python, Lua, etc. ont le concept de coroutines, qui est quelque peu similaire aux coroutines Go, mais il y a deux différences :

Les coroutines Go signifient parallèles (ou peuvent être déployées en parallèle, vous pouvez utiliser le runtime .GOMAXPROCS() spécifie le nombre de CPU pouvant être utilisés simultanément), les coroutines ne sont généralement que concurrentes.

Les coroutines Go communiquent via des canaux ; les coroutines communiquent via les opérations de rendement et de reprise.

La coroutine Go est plus puissante que la coroutine ordinaire, et il est facile de réutiliser la logique de la coroutine dans la coroutine Go. Elle est également très couramment utilisée dans le développement Go. Si vous êtes intéressé, vous pouvez en apprendre davantage à ce sujet. contraste.

Fin

Personnellement, je pense qu'il n'est pas pratique d'implémenter et d'appliquer les coroutines PHP à la main dans une utilisation réelle et les scénarios sont limités, mais comprendre ses concepts et sa mise en œuvre principes est utile. Une meilleure compréhension de la concurrence est utile.

Si vous souhaitez en savoir plus sur les scénarios d'application réels des coroutines, vous pouvez aussi bien essayer le déjà célèbre Swoole. Il encapsule les coroutines sous-jacentes pour les clients de plusieurs protocoles, et peut presque les écrire en programmation synchrone. Réalisez l'effet des E/S asynchrones coroutine.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:segmentfault.com
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal