Maison > interface Web > js tutoriel > le corps du texte

Introduction détaillée au monothreading JavaScript (image)

黄舟
Libérer: 2017-03-10 14:30:37
original
1222 Les gens l'ont consulté

Présentation en détail de certaines choses sur le single-threading en JavaScript (images)

Un camarade de classe m'a récemment posé des questions sur certaines choses concernant le single-threading en JavaScript, et je n'ai pas pu Je n'y répondrai pas. D'accord, j'ai l'impression d'avoir appris JavaScript en vain. Voici quelques éléments que j'ai compilés ces derniers jours sur le monothreading JavaScript.

Tout d’abord, pourquoi JavaScript est-il monothread ?

Comme nous le savons tous, JavaScript s'exécute de manière monothread. Quand on parle de threads, on pense naturellement à des processus. Alors, quel est le lien entre eux ?

Les processus et les threads sont des concepts du système d'exploitation. Un processus est une instance d'exécution d'une application. Chaque processus est composé d'un espace d'adressage virtuel privé, de code, de données et d'autres ressources système que le processus peut appliquer pour créer et utiliser des ressources système (telles que des zones de mémoire indépendantes, etc.) pendant le fonctionnement ; , ces ressources seront également détruites à la fin du processus. Un thread est une unité d'exécution indépendante au sein d'un processus, et les ressources du processus peuvent être partagées entre différents threads. Par conséquent, dans le cas du multithread, une attention particulière doit être accordée au contrôle d'accès aux ressources critiques. Une fois que le système a créé le processus, il démarre le thread principal qui exécute le processus, et le cycle de vie du processus est cohérent avec le cycle de vie du thread principal. La sortie du thread principal signifie la fin et la destruction du processus. Le thread principal est créé par le processus système et les utilisateurs peuvent également créer d'autres threads indépendamment. Cette série de threads s'exécutera simultanément dans le même processus.

De toute évidence, le traitement parallèle des applications peut être réalisé dans un fonctionnement multithread, améliorant ainsi les performances et le débit de l'ensemble de l'application avec une utilisation plus élevée du processeur. Surtout maintenant que de nombreux langages prennent en charge la technologie de traitement parallèle multicœur, JavaScript est exécuté dans un seul thread. Pourquoi ?

En fait, cela a quelque chose à voir avec son objectif. En tant que langage de script de navigateur, l'objectif principal de JavaScript est d'interagir avec les utilisateurs et de manipuler le DOM. Si ces DOM sont exploités de manière multithread, des conflits de fonctionnement peuvent survenir. Supposons que deux threads exécutent un élément DOM en même temps. Le thread 1 nécessite que le navigateur supprime le DOM, tandis que le thread 2 nécessite que le style DOM soit modifié. À ce stade, le navigateur ne peut pas décider quel thread utiliser. Bien sûr, nous pouvons introduire un mécanisme de « verrouillage » dans le navigateur pour résoudre ces conflits, mais cela augmentera considérablement la complexité, c'est pourquoi JavaScript a choisi l'exécution monothread depuis sa naissance.

De plus, comme JavaScript est monothread, il ne peut exécuter qu'une seule tâche spécifique à un certain moment et bloquera l'exécution d'autres tâches. Ainsi, pour les tâches chronophages telles que les E/S, il n’est pas nécessaire d’attendre qu’elles soient terminées avant de poursuivre les opérations suivantes. Avant que ces tâches ne soient terminées, JavaScript peut continuer à effectuer d'autres opérations. Une fois ces tâches chronophages terminées, le traitement correspondant sera effectué sous forme de rappels. Ce sont les fonctionnalités inhérentes à JavaScript : asynchrone et rappels.

Bien sûr, pour les opérations chronophages inévitables (telles que les opérations lourdes, les boucles multiples), HTML5 propose Web Worker, qui utilisera la classe Worker pour créer un thread supplémentaire dans le thread principal d'exécution JavaScript actuel. chargez et exécutez un fichier JavaScript spécifique, ce nouveau thread et le thread principal JavaScript ne s'affecteront pas ni ne bloqueront l'exécution, et le Web Worker fournit une interface pour l'échange de données entre ce nouveau thread et le thread principal JavaScript : événements postMessage et onMessage. Cependant, le DOM ne peut pas être manipulé dans HTML5 Web Worker. Toute tâche nécessitant l'exploitation du DOM doit être confiée au thread principal JavaScript pour son exécution. Par conséquent, bien que HTML5 Web Worker soit introduit, la nature monothread de JavaScript est. toujours pas changé.

Mode de concurrence et boucle d'événement

JavaScript dispose d'un modèle de concurrence basé sur "Event Loop".

Ah, la simultanéité ? N'est-il pas dit que JavaScript est monothread ? Oui, il s'agit bien d'un seul thread, mais il y a une différence entre la concurrence et le parallélisme. La première est une simultanéité logique, tandis que la seconde est une simultanéité physique. Par conséquent, les processeurs monocœur peuvent également réaliser la concurrence.

Introduction détaillée au monothreading JavaScript (image)
Concurrence et parallélisme

Le parallèle est facile à comprendre pour tout le monde, et ce qu'on appelle la « concurrence » signifie que deux événements ou plus se produisent au même intervalle de temps. Comme le montre le premier tableau ci-dessus, puisque le système informatique n'a qu'un seul CPU, les trois programmes ABC utilisent le CPU alternativement dans une perspective "micro", mais le temps d'alternance est très court et l'utilisateur ne peut pas le remarquer, formant une concurrence dans un sens « macro ».

Concept d'exécution

Le contenu suivant explique un modèle théorique. Les moteurs JavaScript modernes se sont concentrés sur la mise en œuvre et l'optimisation de plusieurs des concepts décrits ci-dessous.

Stack (stack)

Voici les tâches que JavaScript exécute. Chaque tâche est appelée une pile de frames.

function f(b){
  var a = 12;
  return a+b+35;
}

function g(x){
  var m = 4;
  return f(m*x);
}

g(21);
Copier après la connexion

Lorsque le code ci-dessus appelle g, il crée la première image de la pile, qui contient les paramètres et variables locales de g. Lorsque g appelle f, le deuxième cadre sera créé et placé au-dessus du premier cadre. Bien entendu, ce cadre contient également les paramètres et variables locales de f. Lorsque f revient, son cadre correspondant sortira de la pile. De la même manière, lorsque g revient, la pile est vide (la définition spécifique de la pile est Last-in, first-out (LIFO)).

Tas

Un nom utilisé pour représenter une grande zone non structurée en mémoire où les objets sont alloués.

File d'attente (file d'attente)

Un runtime JavaScript contient une file d'attente de tâches, qui est composée d'une série de tâches à traiter. Chaque tâche a une fonction correspondante. Lorsque la pile est vide, une tâche sera extraite de la file d'attente des tâches et traitée. Ce processus appelle une série de fonctions associées à la tâche (créant ainsi un cadre de pile initial). Lorsque la tâche est traitée, la pile sera à nouveau vide. (La file d'attente est caractérisée par le premier entré, premier sorti (FIFO)).

Afin de faciliter la description et la compréhension, les conventions suivantes sont établies :

  • La pile est le fil conducteur

  • La file d'attente est la file d'attente des tâches (en attente de planification sur le thread principal pour l'exécution)

OK, les points de connaissances ci-dessus nous aident à clarifier un concept lié au runtime JavaScript, ce qui nous aidera à analyse ultérieure. La

Event Loop

est appelée Event loop car elle est implémentée de la même manière :

while(queue.waitForMessage()){
  queue.processNextMessage();
}
Copier après la connexion

Comme mentionné ci-dessus, la "file d'attente des tâches" est une file d'attente d'événements . Si le périphérique d'E/S termine la tâche ou si l'utilisateur déclenche l'événement (l'événement spécifie une fonction de rappel), alors la fonction de traitement d'événement appropriée entrera dans la « file d'attente des tâches ». " sera planifiée. La première tâche en attente dans le (FIFO). Bien entendu, pour le timer, lorsque l'heure spécifiée est atteinte, la tâche correspondante sera insérée à la fin de la « file d'attente des tâches ».

"Exécuter jusqu'à la fin"

Chaque fois qu'une tâche est exécutée, d'autres tâches seront exécutées. Autrement dit, lorsqu'une fonction s'exécute, elle ne peut pas être remplacée et se terminera avant l'exécution d'un autre code.
Bien sûr, c'est aussi un défaut d'Event Loop : lorsqu'une tâche prend trop de temps à se terminer, l'application ne peut pas gérer les interactions de l'utilisateur (telles que les événements de clic) à temps, et provoque même le crash de l'application. Une meilleure solution consiste à raccourcir le temps d’exécution de la tâche ou à diviser autant que possible une tâche en plusieurs tâches à exécuter.

Ne jamais bloquer

JavaScript est différent des autres langages. L'une des caractéristiques de sa boucle d'événement est qu'il ne bloque jamais. Les opérations d'E/S sont généralement gérées via des événements et des fonctions de rappel. Ainsi, pendant que votre application attend le retour d’une requête asynchrone indexedDB ou XHR, elle peut toujours gérer d’autres opérations (telles que les entrées utilisateur).

Des exceptions existent, comme les alertes ou le XHR synchrone, mais les éviter est considéré comme une bonne pratique. Notez qu'il existe des exceptions aux exceptions (mais généralement dues à des erreurs de mise en œuvre plutôt qu'à d'autres raisons).

Timer

Quelques concepts de timer

Comme mentionné ci-dessus, lorsque l'heure spécifiée est atteinte, le timer insérera la fonction de rappel correspondante à la fin de la "file d'attente des tâches " . Il s'agit de la fonction "minuterie".

Le timer comprend deux méthodes : setTimeout et setInterval. Leur deuxième paramètre spécifie le nombre de millisecondes après lequel la fonction de rappel doit être retardée.
Il y a les choses suivantes à noter à propos du deuxième paramètre :

  • Lorsque le deuxième paramètre est défini par défaut, la valeur par défaut est 0

  • < 🎜 ; >Lorsque la valeur spécifiée est inférieure à 4 millisecondes, augmentez-la à 4 ms (4 ms est spécifié par la norme HTML5, et 10 ms pour les navigateurs de 2010 et avant

Si vous comprenez Avec) ; Avec les connaissances ci-dessus, le code suivant ne devrait poser aucun problème pour vous :

console.log(1);
setTimeout(function(){
    console.log(2);
},10);
console.log(3);
// 输出:1 3 2
Copier après la connexion
Compréhension approfondie des minuteries

Zéro délai setTimeout(func, 0)

Zéro délai ne signifie pas que la fonction de rappel est exécutée immédiatement. Cela dépend si le thread principal est actuellement inactif et les tâches qui l'attendent dans la "file d'attente des tâches".

Regardez le code suivant :

(function () {

  console.log(&#39;this is the start&#39;);

  setTimeout(function cb() {
    console.log(&#39;this is a msg from call back&#39;);
  });

  console.log(&#39;this is just a message&#39;);

  setTimeout(function cb1() {
    console.log(&#39;this is a msg from call back1&#39;);
  }, 0);

  console.log(&#39;this is the  end&#39;);

})();

// 输出如下:
this is the start
this is just a message
this is the end
undefined // 立即调用函数的返回值
this is a msg from callback
this is a msg from a callback1
Copier après la connexion

Le rôle de setTimeout(func, 0)

  • Laissez le rendu du navigateur Modifications actuelles (de nombreux rendus de l'interface utilisateur du navigateur et exécutions de js sont placés dans un thread, et le blocage des threads entraînera l'échec de la mise à jour et du rendu de l'interface)

  • Réévaluer le "scriptis courir trop longtemps" avertissement

  • Modifier l'ordre d'exécution

Regardez à nouveau le code suivant :

<button id=&#39;do&#39;> Do long calc!</button>
<p id=&#39;status&#39;></p>
<p id=&#39;result&#39;></p>

$(&#39;#do&#39;).on(&#39;click&#39;, function(){

  $(&#39;#status&#39;).text(&#39;calculating....&#39;);// 此处会触发redraw事件,但会放到队列里执行,直到long()执行完。

  // 没设定定时器,用户将无法看到“calculating...”
  long();// 执行长时间任务,造成阻塞

  // 设定了定时器,用户就如期看到“calculating...”
  //setTimeout(long,50);// 大约50ms后,将耗时长的long回调函数插入“任务队列”末尾,根据先进先出原则,其将在redraw之后被调度到主线程执行

 });

function long(){
  var result = 0
  for (var i = 0; i<1000; i++){
    for (var j = 0; j<1000; j++){
      for (var k = 0; k<1000; k++){
        result = result + i+j+k
      }
    } 
  }
  $(&#39;#status&#39;).text(&#39;calclation done&#39;); // 在本案例中,该语句必须放到这里,这将使它与回调函数的行为类似
}
Copier après la connexion

La différence entre l'original et la réplique setInterval

Tout le monde sait peut-être que setTimeout peut imiter l'effet de setInterval. Jetons un coup d'œil à la différence entre les codes suivants :

.
// 利用setTimeout模仿setInterval
setTimeout(function(){
    /* 执行一些操作. */
    setTimeout(arguments.callee, 10);
}, 1000);

setInterval(function(){
    /* 执行一些操作 */
}, 1000);
Copier après la connexion
Peut-être pensez-vous qu’il n’y a pas de différence. En effet, lorsque l'opération dans la fonction de rappel prend peu de temps, on ne voit aucune différence entre eux.

其实:上面案例中的 setTimeout 总是会在其回调函数执行后延迟 10ms(或者更多,但不可能少)再次执行回调函数,从而实现setInterval的效果,而 setInterval 总是 10ms 执行一次,而不管它的回调函数执行多久。

所以,如果 setInterval 的回调函数执行时间比你指定的间隔时间相等或者更长,那么其回调函数会连在一起执行。

你可以试试运行以下代码:

var counter = 0;
var initTime = new Date().getTime();
var timer = setInterval(function(){
    if(counter===2){
        clearInterval(timer);
    }
    if(counter === 0){
        for(var i = 0; i < 1990000000; i++){
            ;
        }
    }

    console.log("第"+counter+"次:" + (new Date().getTime() - initTime) + " ms");

    counter++;
},1000);
Copier après la connexion

我电脑Chrome浏览器的输入如下:

第0次:2007 ms
第1次:2013 ms
第2次:3008 ms
Copier après la connexion

浏览器

浏览器不是单线程的

上面说了这么多关于JavaScript是单线程的,下面说说其宿主环境——浏览器。

浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步,一个浏览器至少实现三个常驻线程:

  1. javascript引擎线程 javascript引擎是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序。

  2. GUI渲染线程 GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。但需要注意GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

  3. 浏览器事件触发线程 事件触发线程,当一个事件被触发时该线程会把事件添加到“任务队列”的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS是单线程执行的,所有这些事件都得排队等待JS引擎处理。

在Chrome浏览器中,为了防止因一个标签页奔溃而影响整个浏览器,其每个标签页都是一个进程。当然,对于同一域名下的标签页是能够相互通讯的,具体可看 浏览器跨标签通讯。在Chrome设计中存在很多的进程,并利用进程间通讯来完成它们之间的同步,因此这也是Chrome快速的法宝之一。对于Ajax的请求也需要特殊线程来执行,当需要发送一个Ajax请求时,浏览器会开辟一个新的线程来执行HTTP的请求,它并不会阻塞JavaScript线程的执行,当HTTP请求状态变更时,相应事件会被作为回调放入到“任务队列”中等待被执行。

看看以下代码:

document.onclick = function(){
    console.log("click")
}

for(var i = 0; i< 100000000; i++);
Copier après la connexion

解释一下代码:首先向document注册了一个click事件,然后就执行了一段耗时的for循环,在这段for循环结束前,你可以尝试点击页面。当耗时操作结束后,console控制台就会输出之前点击事件的”click”语句。这视乎证明了点击事件(也包括其它各种事件)是由额外单独的线程触发的,事件触发后就会将回调函数放进了“任务队列”的末尾,等待着JavaScript主线程的执行。

总结

  • JavaScript是单线程的,同一时刻只能执行特定的任务。而浏览器是多线程的。

  • 异步任务(各种浏览器事件、定时器等)都是先添加到“任务队列”(定时器则到达其指定参数时)。当Stack栈(JS主线程)为空时,就会读取Queue队列(任务队列)的第一个任务(队首),然后执行。

JavaScript为了避免复杂性,而实现单线程执行。而今JavaScript却变得越来越不简单了,当然这也是JavaScript迷人的地方。


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:php.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