js hilo único

js hilo único

1. ¿Por qué JavaScript es de un solo subproceso?

Una característica importante del lenguaje JavaScript es de un solo subproceso, es decir, solo se puede hacer una cosa al mismo tiempo. Entonces, ¿por qué JavaScript no puede tener varios subprocesos? Esto puede mejorar la eficiencia.

El hilo único de JavaScript está relacionado con su propósito. Como lenguaje de programación del navegador, el objetivo principal de JavaScript es interactuar con los usuarios y manipular el DOM. Esto determina que solo puede ser de un solo subproceso, de lo contrario traerá problemas de sincronización muy complicados. Por ejemplo, supongamos que JavaScript tiene dos subprocesos al mismo tiempo. Un subproceso agrega contenido a un determinado nodo DOM y el otro subproceso elimina este nodo. En este momento, ¿qué subproceso debe usar el navegador?

Para utilizar la potencia informática de las CPU multinúcleo, HTML5 propone el estándar Web Worker, que permite que los scripts de JavaScript creen múltiples subprocesos, pero los subprocesos secundarios están
completamente controlados por el subproceso principal y no deben operar el DOM. Por lo tanto, este nuevo estándar no cambia la naturaleza de JavaScript de un solo subproceso.

En segundo lugar, la cola de tareas

Un solo hilo significa que todas las tareas deben ponerse en cola y la tarea anterior se termina antes de que se ejecute la siguiente. Si la tarea anterior lleva mucho tiempo, la última tendrá que esperar una eternidad.

Si la cola se debe a una gran cantidad de cálculos y la CPU está demasiado ocupada, está bien, pero en muchos casos la CPU está inactiva porque los dispositivos IO (dispositivos de entrada y salida) son muy lentos (por ejemplo, las operaciones Ajax leen datos de la red) y esperar a que aparezcan los resultados y luego continuar.

Los diseñadores del lenguaje JavaScript se dieron cuenta de que en este momento, el hilo principal puede ignorar por completo el dispositivo IO, suspender las tareas en espera y ejecutar las tareas que se clasifican más tarde. Espere hasta que el dispositivo IO devuelva el resultado, luego retroceda y continúe con la ejecución de la tarea suspendida.

Por lo tanto, todas las tareas se pueden dividir en dos tipos, una son tareas sincrónicas (sincrónicas) y el otro son tareas asincrónicas (asincrónicas). Las tareas síncronas se refieren a tareas en cola para su ejecución en el subproceso principal, y solo la tarea anterior se puede ejecutar antes de que se pueda ejecutar la siguiente; las tareas asíncronas se refieren a tareas que no ingresan al subproceso principal pero ingresan a la "cola de tareas" (tarea cola) Para las tareas, solo cuando la "cola de tareas" informa al subproceso principal que se puede ejecutar una tarea asíncrona, la tarea ingresará al subproceso principal para su ejecución.

En concreto, el mecanismo operativo de ejecución asincrónica es el siguiente. (Lo mismo es cierto para la ejecución sincrónica, porque puede considerarse una ejecución asincrónica sin tareas asincrónicas).
(1) Todas las tareas sincrónicas se ejecutan en el hilo principal, formando una pila de contexto de ejecución.
(2) Además del hilo principal, hay otro "任务队列"(cola de tareas). Siempre que la tarea asincrónica tenga un resultado en ejecución, "任务队列"se coloca un evento en ella.
(3) 一旦"执行栈Después de que se ejecuten todas las tareas síncronas en ", el sistema leerá "任务队列"para ver qué eventos hay en él. Esas tareas asincrónicas correspondientes terminan el estado de espera, ingresan a la pila de ejecución y comienzan la ejecución.
(4) El hilo principal se repite continuamente El tercer paso anterior.

La siguiente figura es un diagrama esquemático del hilo principal y la cola de tareas.
Inserte la descripción de la imagen aquí
Mientras el hilo principal esté vacío, leerá la "cola de tareas", que es el mecanismo operativo de JavaScript. Este proceso se repetirá continuamente.

Tres, eventos y funciones de devolución de llamada

La "cola de tareas" es una cola de eventos (también se puede entender como una cola de mensajes). Cuando un dispositivo IO completa una tarea, se agrega un evento a la "cola de tareas" para indicar que la tarea asíncrona relacionada puede ingresar al " pila de ejecución ". El hilo principal lee la "cola de tareas", que es para leer qué eventos hay en ella.

Además de los eventos de los dispositivos IO, los eventos en la "cola de tareas" también incluyen algunos eventos generados por el usuario (como clics del mouse, desplazamiento de página, etc.). Siempre que se especifique la función de devolución de llamada, estos eventos entrarán en la "cola de tareas" y esperarán a que el hilo principal los lea.

La llamada "devolución de llamada" (callback) es el código que será suspendido por el hilo principal. Las tareas asincrónicas deben especificar una función de devolución de llamada.Cuando el hilo principal comienza a ejecutar la tarea asincrónica, se ejecuta la función de devolución de llamada correspondiente.

La "cola de tareas" es una estructura de datos de primero en entrar, primero en salir, y el hilo principal lee primero los primeros eventos. El proceso de lectura del hilo principal es básicamente automático, tan pronto como se vacíe la pila de ejecución, el primer evento en la "cola de tareas" entrará automáticamente en el hilo principal. Sin embargo, debido a la función de "temporizador" que se menciona más adelante, el hilo principal debe verificar primero el tiempo de ejecución. Ciertos eventos solo pueden regresar al hilo principal después del tiempo especificado.

Cuatro, bucle de eventos

El hilo principal lee los eventos de la "lista de tareas". Este proceso es cíclico, por lo que todo el mecanismo operativo también se denomina Bucle de eventos.

Para comprender mejor el bucle de eventos, consulte la figura siguiente. (Citado del discurso "Ayuda, estoy atrapado en un bucle de eventos" de Philip Roberts)
Inserte la descripción de la imagen aquí

En la figura anterior, cuando se está ejecutando el hilo principal, se generan un montón y una pila. El código de la pila llama a varias API externas y agregan varios eventos (hacer clic, cargar, listo) a la "cola de tareas". Siempre que se ejecute el código en la pila, el hilo principal leerá la "cola de tareas" y ejecutará las funciones de devolución de llamada correspondientes a esos eventos a su vez.

El código de la pila de ejecución (tarea síncrona) siempre se ejecuta antes de leer la "cola de tareas" (tarea asíncrona). Mire el siguiente ejemplo


    var req = new XMLHttpRequest();
    req.open('GET', url);    
    req.onload = function (){
    
    };    
    req.onerror = function (){
    
    };    
    req.send();
    

El método req.send en el código anterior es una operación Ajax para enviar datos al servidor. Es una tarea asíncrona, lo que significa que el sistema leerá la "cola de tareas" solo después de que se ejecute todo el código del script actual. Por tanto, es equivalente a la siguiente redacción.


 var req = new XMLHttpRequest();
    req.open('GET', url);
    req.send();
    req.onload = function (){
    
    };    
    req.onerror = function (){
    
    };   
    

En otras palabras, la parte de la función de devolución de llamada especificada (onload y onerror) no importa antes o después del método send (), porque son parte de la pila de ejecución, y el sistema siempre las ejecuta antes de leer la "cola de tareas". ".

Cinco, temporizador

Además de colocar eventos para tareas asincrónicas, la "cola de tareas" también puede colocar eventos cronometrados, es decir, especificar el tiempo después del cual se ejecutarán ciertos códigos. Esto se denomina función "temporizador", que es el código que se ejecuta con regularidad.

La función del temporizador se completa principalmente con las dos funciones setTimeout () y setInterval (). Sus mecanismos operativos internos son exactamente los mismos, la diferencia es que
el código especificado por el primero se ejecuta una vez y el segundo repetidamente. A continuación, se analiza principalmente setTimeout ().

setTimeout () acepta dos parámetros, el primero es la función de devolución de llamada y el segundo es el número de milisegundos para retrasar la ejecución.


console.log(1);
setTimeout(function(){
    
    console.log(2);},1000);
console.log(3);

El resultado de la ejecución del código anterior es 1, 3, 2, porque setTimeout () retrasa la ejecución de la segunda línea hasta después de 1000 milisegundos.

Si el segundo parámetro de setTimeout () se establece en 0, significa que después de que se ejecuta el código actual (se borra la pila de ejecución), la función de devolución de llamada especificada se ejecutará inmediatamente (intervalo de 0 milisegundos).


setTimeout(function(){
    
    console.log(1);}, 0);
console.log(2);

El resultado de la ejecución del código anterior es siempre 2, 1, porque solo después de que se ejecute la segunda línea, el sistema ejecutará la función de devolución de llamada en la "cola de tareas".

En resumen, el significado de setTimeout (fn, 0) es especificar una tarea que se ejecutará en el tiempo de inactividad disponible más temprano del hilo principal, es decir, que se ejecute lo antes posible. Agrega un evento al final de la "cola de tareas", por lo que no se ejecutará hasta que se procesen la tarea de sincronización y los eventos existentes en la "cola de tareas".

El estándar HTML5 especifica el valor mínimo (intervalo más corto) del segundo parámetro de setTimeout (), que no debe ser menor a 4 milisegundos, si es menor que este valor aumentará automáticamente. Antes de esto, los navegadores más antiguos establecían el intervalo mínimo en 10 milisegundos. Además, para esos cambios de DOM (especialmente aquellos que implican volver a renderizar la página), generalmente no se ejecutan de inmediato, sino cada 16 milisegundos. En este momento, el efecto de usar requestAnimationFrame () es mejor que setTimeout ().

Cabe señalar que setTimeout () solo inserta el evento en la "cola de tareas". Debe esperar hasta que se ejecute el código actual (pila de ejecución) antes de que el hilo principal ejecute su función de devolución de llamada designada. Si el código actual tarda mucho, puede tardar mucho, por lo que no hay garantía de que la función de devolución de llamada se ejecute en el momento especificado por setTimeout ().

6. Bucle de eventos de Node.js

Node.js también es un bucle de eventos de un solo subproceso, pero su mecanismo operativo es diferente del entorno del navegador.

Consulte el diagrama a continuación (autor @BusyRich).

Inserte la descripción de la imagen aquí

Según la figura anterior, el mecanismo operativo de Node.js es el siguiente.

1V8引擎解析JavaScript脚本。
(2)解析后的代码,调用Node API。
(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),
 以异步的方式将任务的执行结果返回给V8引擎。
(4V8引擎再将结果返回给用户。

Además de los dos métodos setTimeout y setInterval, Node.js también proporciona otros dos métodos relacionados con la "cola de tareas": process.nextTick y setImmediate. Pueden ayudarnos a profundizar nuestra comprensión de las "colas de tareas".

El método process.nextTick puede activar la función de devolución de llamada al final de la "pila de ejecución" actual, antes del siguiente bucle de eventos (el hilo principal lee la "cola de tareas"). En otras palabras, la tarea que especifica siempre ocurre antes que todas las tareas asincrónicas. El método setImmediate agrega un evento al final de la "cola de tareas" actual, es decir, la tarea que especifica siempre se ejecuta en el siguiente bucle de eventos, que es muy similar a setTimeout (fn, 0). Consulte el ejemplo a continuación (a través de StackOverflow).


process.nextTick(function A() {
    
    
  console.log(1);
  process.nextTick(function B(){
    
    console.log(2);});
});

setTimeout(function timeout() {
    
    
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED

En el código anterior, debido a que la función de devolución de llamada especificada por el método process.nextTick siempre se activa al final de la "pila de ejecución" actual, no solo la función A se ejecuta antes del tiempo de espera de la función de devolución de llamada especificada por setTimeout, sino también la función B se ejecuta antes del tiempo de espera. Esto significa que si hay varias sentencias process.nextTick (independientemente de si están anidadas), todas se ejecutarán en la "pila de ejecución" actual.

Ahora, mire setImmediate nuevamente.


setImmediate(function A() {
    
    
  console.log(1);
  setImmediate(function B(){
    
    console.log(2);});
});

setTimeout(function timeout() {
    
    
  console.log('TIMEOUT FIRED');
}, 0);

En el código anterior, setImmediate y setTimeout (fn, 0) agregan cada uno una función de devolución de llamada A y un tiempo de espera, los cuales se activan en el siguiente bucle de eventos. Entonces, ¿qué función de devolución de llamada se ejecuta primero? La respuesta es incierta. El resultado de la operación puede ser 1 – TIMEOUT FIRED – 2, o TIMEOUT FIRED – 1–2.

Confusamente, la documentación de Node.js establece que la función de devolución de llamada especificada por setImmediate siempre viene antes de setTimeout. De hecho, esto ocurre solo cuando se llama de forma recursiva.


setImmediate(function (){
    
    
  setImmediate(function A() {
    
    
    console.log(1);
    setImmediate(function B(){
    
    console.log(2);});
  });

  setTimeout(function timeout() {
    
    
    console.log('TIMEOUT FIRED');
  }, 0);
});
// 1
// TIMEOUT FIRED
// 2

En el código anterior, setImmediate y setTimeout están encapsulados en un setImmediate, su resultado de ejecución es siempre 1 – TIMEOUT FIRED – 2, luego la función A debe activarse antes del tiempo de espera. En cuanto a la segunda fila detrás de TIMEOUT FIRED (es decir, la función B se activa después del tiempo de espera), se debe a que setImmediate siempre registra el evento en la siguiente ronda de Event Loop, por lo que la función A y el timeout se ejecutan en el mismo ciclo, y la función B está en el siguiente ciclo Ejecución del ciclo circular.

Por lo tanto, hemos obtenido una diferencia importante entre process.nextTick y setImmediate: múltiples sentencias process.nextTick siempre se ejecutan en la "pila de ejecución" actual a la vez, y múltiples setImmediate pueden requerir que se ejecuten múltiples bucles. De hecho, esta es la razón por la que Node.js versión 10.0 agregó el método setImmediate, de lo contrario, la llamada recursiva a process.nextTick como la siguiente será interminable, ¡y el hilo principal no leerá la "cola de eventos" en absoluto!


process.nextTick(function foo() {
    
    
  process.nextTick(foo);
});

De hecho, ahora si escribe un process.nextTick recursivo, Node.js lanzará una advertencia pidiéndole que lo cambie a setImmediate.

Además, dado que la función de devolución de llamada especificada por process.nextTick se activa en este "bucle de eventos", y setImmediate especifica que se activa en el siguiente "bucle de eventos", es obvio que la primera siempre ocurre antes que la última, y la eficiencia de ejecución también es alta (porque no es necesario comprobar la "cola de tareas")

Supongo que te gusta

Origin blog.csdn.net/WLIULIANBO/article/details/112628427
Recomendado
Clasificación