Maison > Article > développement back-end > Implémentation de coroutines en PHP7
Avant-propos
Je crois que tout le monde a entendu parler du concept de « coroutine ».
Mais certains étudiants ne comprennent pas ce concept, et ne savent pas comment le mettre en œuvre, comment l'utiliser et où l'utiliser. Certaines personnes pensent même que le rendement est une coroutine !
Je crois toujours que si vous ne pouvez pas exprimer avec précision un point de connaissance, je peux penser que vous ne comprenez tout simplement pas.
Si vous avez déjà appris à utiliser PHP pour implémenter des coroutines, vous devez avoir lu l'article de frère Niao : Utiliser des coroutines pour implémenter la planification multi-tâches dans PHP | Wind and Snow Corner
Article de frère Niao a été traduit d'un auteur étranger. La traduction est concise et claire, et des exemples spécifiques sont également donnés.
Le but de la rédaction de cet article est de faire un complément plus complet à l'article de frère Niao. Après tout, les bases de certains étudiants ne sont pas assez bonnes et ils sont confus.
Qu'est-ce qu'une coroutine ?
Tout d'abord, voyons ce qu'est une coroutine.
Vous avez peut-être entendu parler des concepts de « processus » et de « thread ».
Un processus est une instance en cours d'exécution d'un fichier exécutable binaire dans la mémoire de l'ordinateur, tout comme votre fichier .exe est une classe et le processus est la nouvelle instance.
Le processus est l'unité de base d'allocation des ressources et de planification dans le système informatique (ne vous inquiétez pas des processus thread dans l'unité de planification). Chaque processeur ne peut traiter qu'un seul processus à la fois.
La soi-disant concurrence semble simplement que le processeur peut gérer plusieurs choses en même temps. Pour un processeur monocœur, il bascule en fait très rapidement entre différents processus.
La commutation de processus nécessite un appel système. Le CPU doit enregistrer diverses informations sur le processus en cours, et CPUCache sera également détruit.
Donc, si vous ne pouvez pas changer de processus, ne le faites pas sauf si vous y êtes obligé.
Alors, comment parvenir à « La commutation de processus n'est effectuée que si cela est nécessaire » ?
Tout d'abord, les conditions pour qu'un processus soit commuté sont : l'exécution du processus est terminée, la tranche de temps CPU allouée au processus se termine, une interruption se produit dans le système qui doit être traité, ou le le processus attend les ressources nécessaires (blocage du processus), etc. Si vous y réfléchissez, il n'y a rien à dire dans les situations précédentes, mais si vous bloquez et attendez, est-ce du gaspillage ?
En fait, s'il est bloqué, il y a d'autres endroits exécutables pour que notre programme s'exécute, donc nous n'avons pas à attendre bêtement !
Il y a donc des fils de discussion.
Une simple compréhension d'un thread est un "micro-processus" qui exécute spécifiquement une fonction (flux logique).
Nous pouvons donc utiliser des threads pour incarner des fonctions qui peuvent s'exécuter simultanément pendant le processus d'écriture de programmes.
Il existe deux types de threads, l'un est géré et planifié par le noyau.
On dit que tant que le noyau est impliqué dans la gestion et la planification, le coût est très élevé. Ce type de thread résout en fait le problème selon lequel lorsqu'un thread en cours d'exécution dans un processus rencontre un blocage, nous pouvons planifier l'exécution d'un autre thread exécutable, mais il est toujours dans le même processus, il n'y a donc pas de changement de processus.
Il existe un autre type de thread, dont la planification est gérée par les programmeurs qui écrivent eux-mêmes les programmes et est invisible pour le noyau. Ce type de thread est appelé « thread d’espace utilisateur ».
Coroutine peut être comprise comme une sorte de fil de discussion dans l'espace utilisateur.
Coroutine a plusieurs caractéristiques :
La collaboration, car il s'agit d'une stratégie de planification écrite par le programmeur lui-même, elle passe par la collaboration au lieu de la préemption
Lorsque l'utilisateur L'état est créé, commuté et détruit
⚠️ Du point de vue de la programmation, l'idée de coroutine est essentiellement le mécanisme de rendement actif et de reprise du flux de contrôle
générateur souvent utilisé pour implémenter des coroutines
Cela dit, vous devez comprendre le concept de base des coroutines, n'est-ce pas ?
Implémentation PHP de la coroutine
Pas à pas, en commençant par expliquer le concept !
Objet itérable
PHP5 fournit un moyen de définir un objet afin qu'il puisse être parcouru à travers une liste de cellules, par exemple en utilisant une instruction foreach.
Si vous souhaitez implémenter un objet itérable, vous devez implémenter l'interface Iterator :
<?php class MyIterator implements Iterator { private $var = array(); public function __construct($array) { if (is_array($array)) { $this->var = $array; } } public function rewind() { echo "rewinding\n"; reset($this->var); } public function current() { $var = current($this->var); echo "current: $var\n"; return $var; } public function key() { $var = key($this->var); echo "key: $var\n"; return $var; } public function next() { $var = next($this->var); echo "next: $var\n"; return $var; } public function valid() { $var = $this->current() !== false; echo "valid: {$var}\n"; return $var; } } $values = array(1,2,3); $it = new MyIterator($values); foreach ($it as $a => $b) { print "$a: $b\n"; }
Generator
On peut dire que pour pour qu'un objet For soit traversé par foreach, vous devez implémenter un tas de méthodes. Le mot clé rendement est de simplifier ce processus.
Les générateurs offrent un moyen plus simple d'implémenter une itération d'objet simple. Par rapport à la définition d'une classe pour implémenter l'interface Iterator, la surcharge de performances et la complexité sont considérablement réduites.
<?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"; }
Rappelez-vous, si rendement est utilisé dans une fonction, c'est un générateur Il est inutile de l'appeler directement. Il ne peut pas être exécuté comme une fonction !
Donc, le rendement est le rendement. La prochaine fois, celui qui dit que le rendement est une coroutine, je vous régalerai certainement xxxx.
Coroutine PHP
Comme mentionné précédemment lors de l'introduction des coroutines, les coroutines nécessitent que les programmeurs écrivent eux-mêmes le mécanisme de planification. Voyons comment écrire ce mécanisme.
Utilisation correcte des générateurs
Puisque les générateurs ne peuvent pas être appelés directement comme les fonctions, comment peuvent-ils être appelés ?
La méthode est la suivante :
foreach him
send ($value)
current / next...
Task L'implémentation de
Task est l'abstraction d'une tâche. Nous venons de dire qu'une coroutine est un thread de l'espace utilisateur, et un thread peut être compris comme exécutant une fonction.
Le constructeur de Task reçoit donc une fonction de fermeture, que nous nommons coroutine.
/** * Task任务类 */ class Task { protected $taskId; protected $coroutine; protected $beforeFirstYield = true; protected $sendValue; /** * Task constructor. * @param $taskId * @param Generator $coroutine */ public function __construct($taskId, Generator $coroutine) { $this->taskId = $taskId; $this->coroutine = $coroutine; } /** * 获取当前的Task的ID * * @return mixed */ public function getTaskId() { return $this->taskId; } /** * 判断Task执行完毕了没有 * * @return bool */ public function isFinished() { return !$this->coroutine->valid(); } /** * 设置下次要传给协程的值,比如 $id = (yield $xxxx),这个值就给了$id了 * * @param $value */ public function setSendValue($value) { $this->sendValue = $value; } /** * 运行任务 * * @return mixed */ public function run() { // 这里要注意,生成器的开始会reset,所以第一个值要用current获取 if ($this->beforeFirstYield) { $this->beforeFirstYield = false; return $this->coroutine->current(); } else { // 我们说过了,用send去调用一个生成器 $retval = $this->coroutine->send($this->sendValue); $this->sendValue = null; return $retval; } } }
Scheduler实现
接下来就是Scheduler这个重点核心部分,他扮演着调度员的角色。
/** * Class Scheduler */ Class Scheduler { /** * @var SplQueue */ protected $taskQueue; /** * @var int */ protected $tid = 0; /** * Scheduler constructor. */ public function __construct() { /* 原理就是维护了一个队列, * 前面说过,从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制 * */ $this->taskQueue = new SplQueue(); } /** * 增加一个任务 * * @param Generator $task * @return int */ public function addTask(Generator $task) { $tid = $this->tid; $task = new Task($tid, $task); $this->taskQueue->enqueue($task); $this->tid++; return $tid; } /** * 把任务进入队列 * * @param Task $task */ public function schedule(Task $task) { $this->taskQueue->enqueue($task); } /** * 运行调度器 */ public function run() { while (!$this->taskQueue->isEmpty()) { // 任务出队 $task = $this->taskQueue->dequeue(); $res = $task->run(); // 运行任务直到 yield if (!$task->isFinished()) { $this->schedule($task); // 任务如果还没完全执行完毕,入队等下次执行 } } } }
这样我们基本就实现了一个协程调度器。
你可以使用下面的代码来测试:
<?php function task1() { for ($i = 1; $i <= 10; ++$i) { echo "This is task 1 iteration $i.\n"; yield; // 主动让出CPU的执行权 } } function task2() { for ($i = 1; $i <= 5; ++$i) { echo "This is task 2 iteration $i.\n"; yield; // 主动让出CPU的执行权 } } $scheduler = new Scheduler; // 实例化一个调度器 $scheduler->addTask(task1()); // 添加不同的闭包函数作为任务 $scheduler->addTask(task2()); $scheduler->run();
关键说下在哪里能用得到PHP协程。
function task1() { /* 这里有一个远程任务,需要耗时10s,可能是一个远程机器抓取分析远程网址的任务,我们只要提交最后去远程机器拿结果就行了 */ remote_task_commit(); // 这时候请求发出后,我们不要在这里等,主动让出CPU的执行权给task2运行,他不依赖这个结果 yield; yield (remote_task_receive()); ... } function task2() { for ($i = 1; $i <= 5; ++$i) { echo "This is task 2 iteration $i.\n"; yield; // 主动让出CPU的执行权 } }
这样就提高了程序的执行效率。
关于『系统调用』的实现,鸟哥已经讲得很明白,我这里不再说明。
协程堆栈
鸟哥文中还有一个协程堆栈的例子。
我们上面说过了,如果在函数中使用了yield,就不能当做函数使用。
所以你在一个协程函数中嵌套另外一个协程函数:
<?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->addTask(task()); $scheduler->run();
这里的echoTimes是执行不了的!所以就需要协程堆栈。
不过没关系,我们改一改我们刚刚的代码。
把Task中的初始化方法改下,因为我们在运行一个Task的时候,我们要分析出他包含了哪些子协程,然后将子协程用一个堆栈保存。(C语言学的好的同学自然能理解这里,不理解的同学我建议去了解下进程的内存模型是怎么处理函数调用)
/** * Task constructor. * @param $taskId * @param Generator $coroutine */ public function __construct($taskId, Generator $coroutine) { $this->taskId = $taskId; // $this->coroutine = $coroutine; // 换成这个,实际Task->run的就是stackedCoroutine这个函数,不是$coroutine保存的闭包函数了 $this->coroutine = stackedCoroutine($coroutine); }
当Task->run()的时候,一个循环来分析:
/** * @param Generator $gen */ function stackedCoroutine(Generator $gen) { $stack = new SplStack; // 不断遍历这个传进来的生成器 for (; ;) { // $gen可以理解为指向当前运行的协程闭包函数(生成器) $value = $gen->current(); // 获取中断点,也就是yield出来的值 if ($value instanceof Generator) { // 如果是也是一个生成器,这就是子协程了,把当前运行的协程入栈保存 $stack->push($gen); $gen = $value; // 把子协程函数给gen,继续执行,注意接下来就是执行子协程的流程了 continue; } // 我们对子协程返回的结果做了封装,下面讲 $isReturnValue = $value instanceof CoroutineReturnValue; // 子协程返回`$value`需要主协程帮忙处理 if (!$gen->valid() || $isReturnValue) { if ($stack->isEmpty()) { return; } // 如果是gen已经执行完毕,或者遇到子协程需要返回值给主协程去处理 $gen = $stack->pop(); //出栈,得到之前入栈保存的主协程 $gen->send($isReturnValue ? $value->getValue() : NULL); // 调用主协程处理子协程的输出值 continue; } $gen->send(yield $gen->key() => $value); // 继续执行子协程 } }
然后我们增加echoTime的结束标示:
class CoroutineReturnValue { protected $value; public function __construct($value) { $this->value = $value; } // 获取能把子协程的输出值给主协程,作为主协程的send参数 public function getValue() { return $this->value; } } function retval($value) { return new CoroutineReturnValue($value); }
然后修改echoTimes:
function echoTimes($msg, $max) { for ($i = 1; $i <= $max; ++$i) { echo "$msg iteration $i\n"; yield; } yield retval(""); // 增加这个作为结束标示 }
Task变为:
function task1() { yield echoTimes('bar', 5); }
这样就实现了一个协程堆栈,现在你可以举一反三了。
PHP7中yield from关键字
PHP7中增加了yield from,所以我们不需要自己实现携程堆栈,真是太好了。
把Task的构造函数改回去:
public function __construct($taskId, Generator $coroutine) { $this->taskId = $taskId; $this->coroutine = $coroutine; // $this->coroutine = stackedCoroutine($coroutine); //不需要自己实现了,改回之前的 }
echoTimes函数:
function echoTimes($msg, $max) { for ($i = 1; $i <= $max; ++$i) { echo "$msg iteration $i\n"; yield; } }
task1生成器:
function task1() { yield from echoTimes('bar', 5); }
这样,轻松调用子协程。
建议不要使用PHP的Yield来实现协程,推荐使用swoole,2.0已经支持了协程,并附带了部分案例。
推荐教程:《PHP7教程》
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!