Heim  >  Artikel  >  Web-Frontend  >  Detaillierte Einführung in einige Dinge über JavaScript-Single-Threads (Bild)

Detaillierte Einführung in einige Dinge über JavaScript-Single-Threads (Bild)

黄舟
黄舟Original
2017-03-10 14:30:371171Durchsuche

Ausführliche Einführung in einige Dinge über Single-Threading in JavaScript (Bilder)

Ich wurde kürzlich von einem Klassenkameraden nach einigen Dingen über Single-Threading in JavaScript gefragt, und ich konnte' Ich kann es nicht beantworten. Okay, ich habe das Gefühl, dass ich JavaScript umsonst gelernt habe. Hier sind einige Dinge, die ich in den letzten Tagen zum Thema JavaScript-Single-Threading zusammengestellt habe.

Erstens: Warum ist JavaScript Single-Threaded?

Wie wir alle wissen, läuft JavaScript im Single-Thread-Verfahren. Wenn wir über Threads sprechen, denken wir natürlich an Prozesse. Was ist also die Verbindung zwischen ihnen?

Prozesse und Threads sind Konzepte des Betriebssystems. Ein Prozess ist eine Ausführungsinstanz einer Anwendung. Jeder Prozess besteht aus privatem virtuellem Adressraum, Code, Daten und anderen Systemressourcen, die der Prozess zum Erstellen und Verwenden von Systemressourcen (z. B. unabhängigen Speicherbereichen usw.) verwenden kann . Diese Ressourcen werden auch zerstört, wenn der Prozess beendet wird. Ein Thread ist eine unabhängige Ausführungseinheit innerhalb eines Prozesses und Prozessressourcen können von verschiedenen Threads gemeinsam genutzt werden. Daher muss beim Multithreading besonderes Augenmerk auf die Zugriffskontrolle kritischer Ressourcen gelegt werden. Nachdem das System den Prozess erstellt hat, startet es den Haupt-Thread, der den Prozess ausführt. Der Lebenszyklus des Prozesses stimmt mit dem Lebenszyklus des Haupt-Threads überein. Der Hauptthread wird vom Systemprozess erstellt, und Benutzer können auch unabhängig voneinander andere Threads erstellen, die gleichzeitig im selben Prozess ausgeführt werden.

Natürlich kann im Multithread-Betrieb eine parallele Verarbeitung von Anwendungen erreicht werden, wodurch die Leistung und der Durchsatz der gesamten Anwendung bei höherer CPU-Auslastung verbessert werden. Insbesondere jetzt, da viele Sprachen die Multi-Core-Parallelverarbeitungstechnologie unterstützen, wird JavaScript in einem einzelnen Thread ausgeführt.

Eigentlich hat das etwas mit seinem Zweck zu tun. Als Browser-Skriptsprache besteht der Hauptzweck von JavaScript darin, mit Benutzern zu interagieren und das DOM zu manipulieren. Wenn diese DOMs multithreaded betrieben werden, kann es zu Betriebskonflikten kommen. Angenommen, es gibt zwei Threads, die gleichzeitig ein DOM-Element betreiben. Thread 1 erfordert, dass der Browser das DOM löscht, während Thread 2 erfordert, dass der DOM-Stil geändert wird. Zu diesem Zeitpunkt kann der Browser nicht entscheiden, welchen Thread er für den Vorgang verwenden soll . Natürlich können wir einen „Sperr“-Mechanismus in den Browser einführen, um diese Konflikte zu lösen, aber dies erhöht die Komplexität erheblich, weshalb JavaScript seit seiner Geburt die Single-Thread-Ausführung gewählt hat.

Da JavaScript außerdem Single-Threaded ist, kann es nur eine bestimmte Aufgabe zu einem bestimmten Zeitpunkt ausführen und blockiert die Ausführung anderer Aufgaben. Dann müssen Sie bei zeitaufwändigen Aufgaben wie E/A nicht warten, bis sie abgeschlossen sind, bevor Sie mit nachfolgenden Vorgängen fortfahren. Bevor diese Aufgaben abgeschlossen sind, kann JavaScript weiterhin andere Vorgänge ausführen. Wenn diese zeitaufwändigen Aufgaben abgeschlossen sind, wird die entsprechende Verarbeitung in Form von Rückrufen durchgeführt. Dies sind die inhärenten Funktionen von JavaScript: Asynchronität und Rückrufe.

Für unvermeidbare zeitaufwändige Vorgänge (z. B. schwere Vorgänge, mehrere Schleifen) schlägt HTML5 natürlich Web Worker vor, der die Worker-Klasse verwendet, um einen zusätzlichen Thread im aktuellen JavaScript-Ausführungs-Hauptthread zu erstellen Wenn Sie eine bestimmte JavaScript-Datei laden und ausführen, beeinflussen sich dieser neue Thread und der JavaScript-Hauptthread nicht gegenseitig und blockieren auch nicht die Ausführung. Der Web Worker stellt eine Schnittstelle für den Datenaustausch zwischen diesem neuen Thread und dem JavaScript-Hauptthread bereit: postMessage- und onMessage-Ereignisse. Allerdings kann das DOM nicht im HTML5-Web-Worker manipuliert werden, der den Betrieb des DOM zur Ausführung erfordert. Obwohl der HTML5-Web-Worker eingeführt wird, ist dies der Fall immer noch nicht geändert.

Parallelitätsmodus und Ereignisschleife

JavaScript verfügt über ein Parallelitätsmodell, das auf „Ereignisschleife“ basiert.

Ah, Parallelität? Heißt es nicht, dass JavaScript Single-Threaded ist? Ja, es handelt sich tatsächlich um Single-Threaded, aber es gibt einen Unterschied zwischen Parallelität und Parallelität. Ersteres ist logische Gleichzeitigkeit, während letzteres physikalische Gleichzeitigkeit ist. Daher können auch Single-Core-Prozessoren Parallelität erreichen.

Detaillierte Einführung in einige Dinge über JavaScript-Single-Threads (Bild)
Parallelität und Parallelität

Parallel ist für jeden leicht zu verstehen, und die sogenannte „Parallelität“ bedeutet, dass zwei oder mehr Ereignisse im gleichen Zeitintervall auftreten. Wie in der ersten Tabelle oben gezeigt, verwenden die drei Programme ABC die CPU abwechselnd aus einer „Mikro“-Perspektive, da das Computersystem nur über eine CPU verfügt. Die Wechselzeit ist jedoch sehr kurz und der Benutzer kann sie nicht bemerken, wodurch eine Parallelität entsteht „Makro“-Sinn.

Laufzeitkonzept

Der folgende Inhalt erläutert ein theoretisches Modell. Moderne JavaScript-Engines haben sich auf die Implementierung und Optimierung mehrerer der unten beschriebenen Konzepte konzentriert.

Stack (Stack)

Hier sind die Aufgaben, die JavaScript ausführt. Jede Aufgabe wird als Stapel von Frames bezeichnet.

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

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

g(21);

Wenn der obige Code g aufruft, erstellt er den ersten Frame des Stapels, der die Parameter und lokalen Variablen von g enthält. Wenn g f aufruft, wird der zweite Frame erstellt und über dem ersten Frame platziert. Natürlich enthält dieser Frame auch die Parameter und lokalen Variablen von f. Wenn f zurückkehrt, wird der entsprechende Frame vom Stapel entfernt. Auf die gleiche Weise ist der Stapel leer, wenn g zurückgegeben wird (die spezifische Definition des Stapels lautet Last-in-first-out (LIFO)).

Heap

Ein Name, der einen großen unstrukturierten Bereich im Speicher darstellt, in dem Objekte zugewiesen werden.

Warteschlange (Warteschlange)

Eine JavaScript-Laufzeit enthält eine Aufgabenwarteschlange, die aus einer Reihe von zu verarbeitenden Aufgaben besteht. Jede Aufgabe hat eine entsprechende Funktion. Wenn der Stapel leer ist, wird eine Aufgabe aus der Aufgabenwarteschlange entnommen und verarbeitet. Dieser Prozess ruft eine Reihe von Funktionen auf, die der Aufgabe zugeordnet sind (und erstellt so einen anfänglichen Stapelrahmen). Wenn die Aufgabe abgearbeitet ist, ist der Stapel wieder leer. (Die Warteschlange ist durch First-in-First-out (FIFO) gekennzeichnet.)

Um die Beschreibung und das Verständnis zu erleichtern, werden die folgenden Konventionen getroffen:

  • Stapelstapel ist der Hauptthread

  • Warteschlangenwarteschlange ist die Aufgabenwarteschlange (Warten auf die Planung zum Hauptthread zur Ausführung)

OK, die oben genannten Wissenspunkte helfen uns dabei, ein Konzept im Zusammenhang mit der JavaScript-Laufzeit zu klären, das dabei hilft anschließende Analyse.

Ereignisschleife

wird Ereignisschleife genannt, weil sie auf ähnliche Weise implementiert ist:

while(queue.waitForMessage()){
  queue.processNextMessage();
}

Wie oben erwähnt, ist die „Aufgabenwarteschlange“ eine Warteschlange von Ereignissen Wenn das E/A-Gerät die Aufgabe abschließt oder der Benutzer das Ereignis auslöst (das Ereignis gibt eine Rückruffunktion an), wird die entsprechende Ereignisverarbeitungsfunktion in die „Aufgabenwarteschlange“ eingegeben " wird geplant. Die erste ausstehende Aufgabe im (FIFO). Natürlich wird für den Timer bei Erreichen der angegebenen Zeit die entsprechende Aufgabe am Ende der „Aufgabenwarteschlange“ eingefügt.

„Bis zum Abschluss ausführen“

Immer wenn eine Aufgabe ausgeführt wird, werden andere Aufgaben ausgeführt. Das heißt, wenn eine Funktion ausgeführt wird, kann sie nicht ersetzt werden und wird abgeschlossen, bevor anderer Code ausgeführt wird.
Natürlich ist dies auch ein Manko der Ereignisschleife: Wenn die Ausführung einer Aufgabe zu lange dauert, kann die Anwendung Benutzerinteraktionen (z. B. Klickereignisse) nicht rechtzeitig verarbeiten und führt sogar zum Absturz der Anwendung. Eine bessere Lösung besteht darin, die Zeit für die Erledigung der Aufgabe zu verkürzen oder eine Aufgabe zur Ausführung so weit wie möglich in mehrere Aufgaben aufzuteilen.

Niemals blockierend

JavaScript unterscheidet sich von anderen Sprachen dadurch, dass es niemals blockiert. E/A-Vorgänge werden normalerweise über Ereignisse und Rückruffunktionen abgewickelt. Während Ihre Anwendung auf die Rückgabe einer asynchronen IndexedDB- oder XHR-Anfrage wartet, kann sie dennoch andere Vorgänge (z. B. Benutzereingaben) verarbeiten.

Es gibt Ausnahmen, wie z. B. Warnungen oder synchrones XHR, aber ihre Vermeidung gilt als bewährte Vorgehensweise. Beachten Sie, dass es Ausnahmen von Ausnahmen gibt (normalerweise jedoch aufgrund von Implementierungsfehlern und nicht aus anderen Gründen).

Timer

Einige Konzepte des Timers

Wie oben erwähnt, fügt der Timer bei Erreichen der angegebenen Zeit die entsprechende Rückruffunktion am Ende der „Aufgabenwarteschlange“ ein " . Dies ist die „Timer“-Funktion.

Der Timer umfasst zwei Methoden: setTimeout und setInterval. Ihr zweiter Parameter gibt die Anzahl der Millisekunden an, nach denen die Rückruffunktion verzögert wird.
Über den zweiten Parameter sind folgende Dinge zu beachten:

  • Wenn der zweite Parameter standardmäßig 0 ist;

  • Wenn der angegebene Wert weniger als 4 Millisekunden beträgt, erhöhen Sie ihn auf 4 ms (4 ms sind im HTML5-Standard angegeben, und 10 ms für Browser aus dem Jahr 2010 und früher);

Wenn Sie With verstehen Mit den oben genannten Kenntnissen sollte der folgende Code kein Problem für Sie sein:

console.log(1);
setTimeout(function(){
    console.log(2);
},10);
console.log(3);
// 输出:1 3 2

Vertiefendes Verständnis von Timern

Zero Delay SetTimeout(func, 0)

Eine Verzögerung von Null bedeutet nicht, dass die Rückruffunktion sofort ausgeführt wird. Es hängt davon ab, ob der Hauptthread gerade inaktiv ist und welche Aufgaben davor in der „Aufgabenwarteschlange“ warten.

Sehen Sie sich den folgenden Code an:

(function () {

  console.log('this is the start');

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

  console.log('this is just a message');

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

  console.log('this is the  end');

})();

// 输出如下:
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

Die Rolle von setTimeout(func, 0)

  • Lassen Sie die Browser-Rendering Aktuelle Änderungen (Viele Browser-UI-Renderings und JS-Ausführungen werden in einem Thread platziert, und das Blockieren von Threads führt dazu, dass die Schnittstelle nicht aktualisiert und gerendert werden kann)

  • Bewerten Sie die „scriptis läuft zu lange“ Warnung

  • Ändern Sie die Ausführungsreihenfolge

Sehen Sie sich den folgenden Code noch einmal an:

<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;); // 在本案例中,该语句必须放到这里,这将使它与回调函数的行为类似
}

Der Unterschied zwischen dem Original und dem Replikat setInterval

Jeder weiß vielleicht, dass setTimeout den Effekt von setInterval imitieren kann. Schauen wir uns den Unterschied zwischen den folgenden Codes an:

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

setInterval(function(){
    /* 执行一些操作 */
}, 1000);

Vielleicht denken Sie, dass es keinen Unterschied gibt. Wenn die Operation in der Rückruffunktion nur kurze Zeit dauert, können Sie tatsächlich keinen Unterschied zwischen ihnen erkennen.

其实:上面案例中的 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);

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

第0次:2007 ms
第1次:2013 ms
第2次:3008 ms

浏览器

浏览器不是单线程的

上面说了这么多关于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++);

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

总结

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

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

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


Das obige ist der detaillierte Inhalt vonDetaillierte Einführung in einige Dinge über JavaScript-Single-Threads (Bild). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn