Maison > interface Web > js tutoriel > Parlons en profondeur du mécanisme sous-jacent de mise en œuvre et d'exécution des boucles asynchrones et événementielles de Node.

Parlons en profondeur du mécanisme sous-jacent de mise en œuvre et d'exécution des boucles asynchrones et événementielles de Node.

青灯夜游
Libérer: 2022-07-20 20:23:14
avant
2208 Les gens l'ont consulté

Parlons en profondeur du mécanisme sous-jacent de mise en œuvre et d'exécution des boucles asynchrones et événementielles de Node.

Node est né à l'origine pour créer un serveur Web hautes performances. En tant que moteur d'exécution côté serveur pour JavaScript, il possède des fonctionnalités telles que les E/S asynchrones basées sur les événements et le thread unique. Le modèle de programmation asynchrone basé sur la boucle d'événements permet à Node de gérer une concurrence élevée et améliore considérablement les performances du serveur. En même temps, car il conserve les caractéristiques monothread de JavaScript, Node n'a pas besoin de gérer des problèmes tels que la synchronisation des états et. impasse sous multi-threads. Il n’y a pas de surcharge de performances causée par le changement de contexte de thread. Sur la base de ces caractéristiques, Node présente les avantages inhérents d'une haute performance et d'une concurrence élevée, et diverses plates-formes d'applications réseau à haut débit et évolutives peuvent être construites sur cette base.

Cet article abordera le mécanisme sous-jacent d'implémentation et d'exécution de la boucle asynchrone et d'événements Node, j'espère qu'il vous sera utile.

Pourquoi asynchrone ?

Pourquoi Node utilise-t-il l'asynchrone comme modèle de programmation de base ?

Comme mentionné précédemment, Node est né à l'origine pour créer des serveurs Web hautes performances. En supposant qu'il existe plusieurs ensembles de tâches non liées à accomplir dans le scénario commercial, il existe deux solutions grand public modernes :

  • Un seul thread exécuté en série. .

  • Plusieurs fils de discussion à compléter en parallèle.

L'exécution série monothread est un modèle de programmation synchrone, bien qu'elle soit plus conforme à la façon de penser du programmeur et qu'il soit plus facile d'écrire du code plus pratique, car elle exécute les E/S de manière synchrone, la même chose. une seule requête peut être traitée à la fois, ce qui entraînera une réponse lente du serveur et ne pourra pas être utilisé dans des scénarios d'application à haute concurrence. De plus, comme il bloque les E/S, le processeur attendra toujours que les E/S se terminent et. ne peut pas faire autre chose, ce qui réduit la puissance de traitement du processeur. La puissance de traitement n'est pas entièrement utilisée, ce qui conduit finalement à une faible efficacité. Le modèle de programmation multithread donnera également des maux de tête aux développeurs en raison de problèmes tels que la synchronisation des états et le blocage de la programmation. Bien que le multithreading puisse améliorer efficacement l’utilisation du processeur sur les processeurs multicœurs.

Bien que le modèle de programmation d'exécution série monothread et d'exécution parallèle multithread ait ses propres avantages, il présente également des inconvénients en termes de performances et de difficulté de développement.

De plus, à partir de la vitesse de réponse aux demandes du client, si le client obtient deux ressources en même temps, la vitesse de réponse de la méthode synchrone sera la somme des vitesses de réponse des deux ressources, tandis que la vitesse de réponse de la méthode asynchrone sera deux. Le plus grand d'entre eux, l'avantage en termes de performances est très évident par rapport à la synchronisation. À mesure que la complexité de l'application augmente, ce scénario évoluera vers la réponse à n requêtes en même temps, et les avantages de l'asynchrone par rapport à la synchronisation seront mis en évidence.

Pour résumer, Node donne sa réponse : utilisez un seul thread pour éviter les blocages multi-thread, la synchronisation d'état et d'autres problèmes ; utilisez les E/S asynchrones pour éviter qu'un seul thread ne se bloque afin de mieux utiliser le processeur. C'est pourquoi Node utilise async comme modèle de programmation de base.

De plus, afin de combler les lacunes d'un thread unique qui ne peut pas utiliser de processeurs multicœurs, Node fournit également un sous-processus similaire aux Web Workers dans le navigateur, qui peut utiliser efficacement le processeur via des processus de travail.

Comment implémenter l'asynchrone ?

Après avoir expliqué pourquoi nous devrions utiliser l'asynchrone, comment implémenter l'asynchrone ?

Il existe deux types d'opérations asynchrones que nous appelons habituellement : l'une concerne les opérations liées aux E/S comme les E/S de fichiers et les E/S réseau ; l'autre concerne les opérations non liées aux E/S comme

. Évidemment, l'asynchrone dont nous parlons fait référence aux opérations liées aux E/S, c'est-à-dire aux E/S asynchrones.

setTimeOutsetIntervalLes E/S asynchrones sont proposées dans l'espoir que les appels d'E/S ne bloqueront pas l'exécution des programmes suivants, et que le temps d'attente initial pour la fin des E/S sera alloué aux autres activités requises pour l'exécution. Pour atteindre cet objectif, des E/S non bloquantes sont nécessaires.

Le blocage des E/S signifie qu'une fois que le processeur a lancé un appel d'E/S, il se bloquera jusqu'à ce que les E/S soient terminées. Connaissant les E/S bloquantes, les E/S non bloquantes sont faciles à comprendre. Le CPU reviendra immédiatement après le lancement de l'appel d'E/S au lieu de bloquer et d'attendre. Le CPU peut gérer d'autres transactions avant que les E/S ne soient terminées. De toute évidence, par rapport aux E/S bloquantes, les E/S non bloquantes améliorent davantage les performances.

Donc, puisque des E/S non bloquantes sont utilisées et que le CPU peut revenir immédiatement après avoir émis un appel d'E/S, comment sait-il que les E/S sont terminées ? La réponse est un sondage.

Afin d'obtenir l'état des appels d'E/S en temps opportun, le CPU appellera en continu les opérations d'E/S à plusieurs reprises pour confirmer si les E/S ont été terminées. Cette technologie d'appels répétés permet de déterminer si l'opération est terminée. terminé est appelé sondage.

Évidemment, l'interrogation amènera le processeur à effectuer des jugements d'état à plusieurs reprises, ce qui constitue un gaspillage de ressources CPU. De plus, l'intervalle d'interrogation est difficile à contrôler si l'intervalle est trop long, l'achèvement de l'opération d'E/S ne recevra pas de réponse dans les délais, ce qui réduit indirectement la vitesse de réponse de l'application si l'intervalle est trop court ; Le processeur sera inévitablement dépensé en interrogation. Cela prend plus de temps et réduit l'utilisation des ressources du processeur.

Par conséquent, bien que l'interrogation réponde à l'exigence selon laquelle les E/S non bloquantes ne bloquent pas l'exécution des programmes suivants, pour l'application, elle ne peut toujours être considérée que comme une sorte de synchronisation, car l'application doit toujours attendre le E/S à terminer Au retour, il a fallu encore attendre beaucoup de temps.

L'E/S asynchrone parfaite que nous attendons devrait être que l'application lance des appels non bloquants. Il n'est pas nécessaire d'interroger en permanence l'état des appels d'E/S via une interrogation. Au lieu de cela, la tâche suivante peut être traitée directement et l'I. /O est terminé, puis transmettez les données à l'application via un sémaphore ou un rappel.

Comment implémenter ces E/S asynchrones ? La réponse est le pool de threads.

Bien que cet article ait toujours mentionné que Node est exécuté dans un seul thread, le seul thread signifie ici que le code JavaScript est exécuté sur un seul thread pour des parties telles que les opérations d'E/S qui n'ont rien à voir avec l'activité principale. Logique, run L'implémenter dans d'autres threads n'affectera ni ne bloquera le fonctionnement du thread principal. Au contraire, cela peut améliorer l'efficacité d'exécution du thread principal et réaliser des E/S asynchrones.

Grâce au pool de threads, laissez le thread principal effectuer uniquement des appels d'E/S, laissez les autres threads effectuer des E/S bloquantes ou des E/S non bloquantes ainsi que la technologie d'interrogation pour terminer l'acquisition de données, puis utilisez la communication entre les threads pour transférer les E/S /O transfèrent les données obtenues, ce qui facilite la mise en œuvre d'E/S asynchrones :

Parlons en profondeur du mécanisme sous-jacent de mise en œuvre et dexécution des boucles asynchrones et événementielles de Node.

Le thread principal effectue des appels d'E/S, tandis que le pool de threads effectue des opérations d'E/S, termine l'acquisition de données, puis les transmet entre les threads. La communication transfère les données au thread principal pour terminer un appel d'E/S. Le thread principal utilise ensuite la fonction de rappel pour exposer les données à l'utilisateur, qui utilise ensuite les données pour terminer. opérations au niveau de la logique métier. Il s’agit d’un appel d’E/S dans le processus d’E/S asynchrone complet. Pour les utilisateurs, il n'est pas nécessaire de se soucier des détails fastidieux d'implémentation de la couche sous-jacente. Il leur suffit d'appeler l'API asynchrone encapsulée par Node et de transmettre la fonction de rappel qui gère la logique métier, comme indiqué ci-dessous : La couche sous-jacente asynchrone. de

const fs = require("fs");

fs.readFile('example.js', (data) => {
  // 进行业务逻辑的处理
});
Copier après la connexion

Nodejs Le mécanisme d'implémentation diffère selon les plateformes : sous Windows, IOCP est principalement utilisé pour envoyer des appels d'E/S au noyau système et obtenir des opérations d'E/S terminées depuis le noyau, couplé à une boucle d'événements pour compléter le processus d'E/S asynchrone ; ce processus est implémenté via epoll sous Linux ; via kqueue sous FreeBSD et via les ports Event sous Solaris. Le pool de threads est directement fourni par le noyau (IOCP) sous Windows, et la série *nix est implémentée par libuv elle-même. *nix 系列则由 libuv 自行实现。

由于 Windows 平台和 *nix 平台的差异,Node 提供了 libuv 作为抽象封装层,使得所有平台兼容性的判断都由这一层来完成,保证上层的 Node 与下层的自定义线程池及 IOCP 之间各自独立。Node 在编译期间会判断平台条件,选择性编译 unix 目录或是 win 目录下的源文件到目标程序中:

Parlons en profondeur du mécanisme sous-jacent de mise en œuvre et dexécution des boucles asynchrones et événementielles de Node.

以上就是 Node 对异步的实现。

(线程池的大小可以通过环境变量 UV_THREADPOOL_SIZE 设置,默认值为 4,用户可结合实际情况来调整这个值的大小。)

那么问题来了,在得到线程池传递过来的数据后,主线程是如何、何时调用回调函数的呢?答案是事件循环。

基于事件循环的异步编程模型

既然使用回调函数来进行对 I/O 数据的处理,就必然涉及到何时、如何调用回调函数的问题。在实际开发中,往往会涉及到多个、多类异步 I/O 调用的场景,如何合理安排这些异步 I/O 回调的调用,确保异步回调的有序进行是一个难题,而且,除了异步 I/O 之外,还存在定时器这类非 I/O 的异步调用,这类 API 实时性强,优先级相应地更高,如何实现不同优先级回调地调度呢?

因此,必须存在一个调度机制,对不同优先级、不同类型的异步任务进行协调,确保这些任务在主线程上有条不紊地运行。与浏览器一样,Node 选择了事件循环来承担这项重任。

Node 根据任务的种类和优先级将它们分为七类:Timers、Pending、Idle、Prepare、Poll、Check、Close。对于每类任务,都存在一个先进先出的任务队列来存放任务及其回调(Timers 是用小顶堆存放)。基于这七个类型,Node 将事件循环的执行分为如下七个阶段:

timers

这个阶段的执行优先级是最高的。

事件循环在这个阶段会检查存放定时器的数据结构(最小堆),对其中的定时器进行遍历,逐个比较当前时间和过期时间,判断该定时器是否过期,如果过期的话,就将该定时器的回调函数取出并执行。

pending

该阶段会执行网络、IO 等异常时的回调。一些 *nix

En raison de la différence entre la plate-forme Windows et la plate-forme *nix, Node fournit libuv comme couche d'encapsulation abstraite, de sorte que tous les jugements de compatibilité de la plate-forme soient complétés par cette couche, garantissant que la couche supérieure Node et le nœud de couche inférieure est constitué de pools de threads personnalisés et IOCP sont indépendants les uns des autres. Node déterminera les conditions de la plate-forme lors de la compilation et compilera sélectivement les fichiers sources dans le répertoire unix ou le répertoire win dans le programme cible :

Parlons en profondeur du mécanisme sous-jacent de mise en œuvre et dexécution des boucles asynchrones et événementielles de Node.

Ce qui précède est l'implémentation asynchrone de Node. 🎜🎜 (La taille du pool de threads peut être définie via la variable d'environnement UV_THREADPOOL_SIZE. La valeur par défaut est 4. Les utilisateurs peuvent ajuster la taille de cette valeur en fonction de la situation réelle.) 🎜🎜Ensuite, le La question est, après avoir obtenu le pool de threads. Après avoir transmis les données, comment et quand le thread principal appelle-t-il la fonction de rappel ? La réponse est la boucle d'événements. 🎜

Modèle de programmation asynchrone basé sur une boucle d'événements

🎜Étant donné que les fonctions de rappel sont utilisées pour traiter les données d'E/S, cela doit impliquer quand et comment appeler le problème de la fonction de rappel. Dans le développement réel, des scénarios d'appels d'E/S asynchrones multiples et multi-types sont souvent impliqués. Comment organiser raisonnablement les appels de ces rappels d'E/S asynchrones et garantir le déroulement ordonné des rappels asynchrones est un problème difficile. E/S asynchrones En plus de /O, il existe également des appels asynchrones non-E/S tels que des minuteries. Ces API sont hautement en temps réel et ont des priorités en conséquence plus élevées. Comment planifier des rappels avec des priorités différentes ? 🎜🎜Par conséquent, il doit y avoir un mécanisme de planification pour coordonner les tâches asynchrones de différentes priorités et types afin de garantir que ces tâches s'exécutent de manière ordonnée sur le thread principal. Comme les navigateurs, Node a choisi la boucle d’événements pour faire ce gros travail. 🎜🎜Node divise les tâches en sept catégories selon leur type et leur priorité : Minuteurs, En attente, Inactif, Préparer, Sonder, Vérifier, Fermer. Pour chaque type de tâche, il existe une file d'attente de tâches premier entré, premier sorti pour stocker les tâches et leurs rappels (les minuteries sont stockées dans un petit tas supérieur). Sur la base de ces sept types, Node divise l'exécution de la boucle d'événements en sept étapes suivantes : 🎜

timers🎜🎜La priorité d'exécution de cette étape est la plus élevée. 🎜🎜À ce stade, la boucle d'événements vérifiera la structure de données (tas minimum) qui stocke le minuteur, parcourra les minuteurs qu'elle contient, comparera l'heure actuelle et l'heure d'expiration un par un et déterminera si le minuteur a expiré. a expiré, le minuteur sera La fonction de rappel du minuteur est retirée et exécutée. 🎜

en attente🎜🎜Cette étape exécutera des rappels lorsque le réseau, les IO et d'autres exceptions se produisent. Certaines erreurs signalées par *nix seront traitées à ce stade. De plus, certains rappels d'E/S qui devaient être exécutés lors de la phase d'interrogation du cycle précédent seront reportés à cette phase. 🎜🎜Idle and Prepare🎜🎜Ces deux étapes ne sont utilisées qu'à l'intérieur de la boucle de l'événement. 🎜

poll

检索新的 I/O 事件;执行与 I/O 相关的回调(除了关闭回调、定时器调度的回调和 之外几乎所有回调setImmediate());节点会在适当的时候阻塞在这里。

poll,即轮询阶段是事件循环最重要的阶段,网络 I/O、文件 I/O 的回调都主要在这个阶段被处理。该阶段有两个主要功能:

  • 计算该阶段应该阻塞和轮询 I/O 的时间。

  • 处理 I/O 队列中的回调。

当事件循环进入 poll 阶段并且没有设置定时器时:

  • 如果轮询队列不为空,则事件循环将遍历该队列,同步地执行它们,直到队列为空或达到可执行的最大数量。

  • 如果轮询队列为空,则会发生另外两种情况之一:

    • 如果有 setImmediate() 回调需要执行,则立即结束 poll 阶段,并进入 check 阶段以执行回调。

    • 如果没有 setImmediate() 回调需要执行,事件循环将停留在该阶段以等待回调被添加到队列中,然后立即执行它们。在超时时间到达前,事件循环会一直停留等待。之所以选择停留在这里是因为 Node 主要是处理 IO 的,这样可以更及时地响应 IO。

一旦轮询队列为空,事件循环将检查已达到时间阈值的定时器。如果有一个或多个定时器达到时间阈值,事件循环将回到 timers 阶段以执行这些定时器的回调。

check

该阶段会依次执行 setImmediate() 的回调。

close

该阶段会执行一些关闭资源的回调,如 socket.on('close', ...)。该阶段晚点执行也影响不大,优先级最低。

当 Node 进程启动时,它会初始化事件循环,执行用户的输入代码,进行相应异步 API 的调用、计时器的调度等等,然后开始进入事件循环:

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<p>事件循环的每一轮循环(通常被称为 tick),会按照如上给定的优先级顺序进入七个阶段的执行,每个阶段会执行一定数量的队列中的回调,之所以只执行一定数量而不全部执行完,是为了防止当前阶段执行时间过长,避免下一个阶段得不到执行。</p><p>OK,以上就是事件循环的基本执行流程。现在让我们来看另外一个问题。</p><p>对于以下这个场景:</p><pre class="brush:php;toolbar:false">const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});
Copier après la connexion

当服务成功绑定到 8000 端口,即 listen() 成功调用时,此时 listening 事件的回调还没有绑定,因此端口成功绑定后,我们所传入的 listening 事件的回调并不会执行。

再思考另外一个问题,我们在开发中可能会有一些需求,如处理错误、清理不需要的资源等等优先级不是那么高的任务,如果以同步的方式执行这些逻辑,就会影响当前任务的执行效率;如果以异步的方式,比如以回调的形式传入 setImmediate() 又无法保证它们的执行时机,实时性不高。那么要如何处理这些逻辑呢?

基于这几个问题,Node 参考了浏览器,也实现了一套微任务的机制。在 Node 中,除了调用 new Promise().then() 所传入的回调函数会被封装成微任务外,process.nextTick() 的回调也会被封装成微任务,并且后者的执行优先级比前者高。

有了微任务后,事件循环的执行流程又是怎么样的呢?换句话说,微任务的执行时机在什么时候?

  • 在 node 11 及 11 之后的版本,一旦执行完一个阶段里的一个任务就立刻执行微任务队列,清空该队列。

  • 在 node11 之前执行完一个阶段后才开始执行微任务。

因此,有了微任务后,事件循环的每一轮循环,会先执行 timers 阶段的一个任务,然后按照先后顺序清空 process.nextTick()new Promise().then() 的微任务队列,接着继续执行 timers 阶段的下一个任务或者下一个阶段,即 pending 阶段的一个任务,按照这样的顺序以此类推。

利用 process.nextTick(),Node 就可以解决上面的端口绑定问题:在 listen() 方法内部,listening 事件的发出会被封装成回调传入 process.nextTick() 中,如下伪代码所示:

function listen() {
    // 进行监听端口的操作
    ...
    // 将 `listening` 事件的发出封装成回调传入 `process.nextTick()` 中
    process.nextTick(() => {
        emit('listening');
    });
};
Copier après la connexion

在当前代码执行完毕后便会开始执行微任务,从而发出 listening 事件,触发该事件回调的调用。

一些注意事项

由于异步本身的不可预知性和复杂性,在使用 Node 提供的异步 API 的过程中,尽管我们已经掌握了事件循环的执行原理,但是仍可能会有一些不符合直觉或预期的现象产生。

比如定时器(setTimeoutsetImmediate)的执行顺序会因为调用它们的上下文而有所不同。如果两者都是从顶层上下文中调用的,那么它们的执行时间取决于进程或机器的性能。

我们来看以下这个例子:

setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});
Copier après la connexion

以上代码的执行结果是什么呢?按照我们刚才对事件循环的描述,你可能会有这样的答案:由于 timers 阶段会比 check 阶段先执行,因此 setTimeout() 的回调会先执行,然后再执行 setImmediate() 的回调。

实际上,这段代码的输出结果是不确定的,可能先输出 timeout,也可能先输出 immediate。这是因为这两个定时器都是在全局上下文中调用的,当事件循环开始运行并执行到 timers 阶段时,当前时间可能大于 1 ms,也可能不足 1 ms,具体取决于机器的执行性能,因此 setTimeout() 在第一个 timers 阶段是否会被执行实际上是不确定的,因此才会出现不同的输出结果。

(当 delaysetTimeout 的第二个参数)的值大于 2147483647 或小于 1 时, delay 会被设置为 1。)

我们接着看下面这段代码:

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
Copier après la connexion

可以看到,在这段代码中两个定时器都被封装成回调函数传入 readFile 中,很明显当该回调被调用时当前时间肯定大于 1 ms 了,所以 setTimeout 的回调会比 setImmediate 的回调先得到调用,因此打印结果为:timeout immediate

以上是在使用 Node 时需要注意的与定时器相关的事项。除此之外,还需注意 process.nextTick()new Promise().then() 还有 setImmediate() 的执行顺序,由于这部分比较简单,前面已经提到过,就不再赘述了。

总结

文章开篇从为什么要异步、如何实现异步两个角度出发,较详细地阐述了 Node 事件循环的实现原理,并提到一些需要注意的相关事项,希望对你有所帮助。

更多node相关知识,请访问:nodejs 教程

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:juejin.cn
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