JavaScript je jednovláknový jazyk a zároveň neblokující, asynchronní a souběžný. Tento článek vám vysvětlí, jak se to děje.
Runtime
JavaScript je interpretovaný, nikoli kompilovaný jazyk. To znamená, že potřebuje interpretr, který převede JS kód do strojového kódu. Existuje několik typů překladačů (tzv. motorů). Nejoblíbenějšími enginy prohlížečů jsou V8 (Chrome), Quantum (Firefox) a WebKit (Safari). Mimochodem, V8 se používá také v populárním běhovém prostředí, které není určeno pro prohlížeče, Node.js.
Každý engine obsahuje paměťovou haldu, zásobník volání, smyčku událostí, frontu zpětných volání a rozhraní WebAPI s požadavky HTTP, časovači, událostmi atd., vše implementováno vlastním způsobem pro rychlejší a bezpečnější interpretaci kódu JS.
Základní architektura běhu JS. Autor: Alex Zlatkov
Jedno vlákno
Jednovláknový jazyk je jazyk s jediným zásobníkem volání a jedinou paměťovou hromadou. To znamená, že v jednom okamžiku běží pouze jedna věc.
A zásobník je souvislá oblast paměti, která alokuje lokální kontext pro každou prováděnou funkci.
A hromada je mnohem větší oblast, kde je uloženo vše, co je dynamicky alokováno.
A zásobník volání je datová struktura, která v podstatě zaznamenává, kde se v programu nacházíme.
Zásobník volání
Napišme si jednoduchý kód a sledujme, co se děje na zásobníku volání.
Jak vidíte, funkce jsou přidávány do zásobníku, prováděny a později mazány. Jedná se o takzvaný způsob LIFO - Last In, First Out. Každá položka v zásobníku volání se nazývá rám zásobníku.
Znalost zásobníku volání je užitečná při čtení stop zásobníku chyb. Přesná příčina chyby je zpravidla nahoře v prvním řádku, ačkoli pořadí provádění kódu je zdola nahoru.
Někdy se můžete vypořádat s oblíbenou chybou, na kterou vás upozorní Překročení maximální velikosti zásobníku volání. Toho lze snadno dosáhnout pomocí rekurze:
funkce foo() {
foo()
}
foo()
a náš prohlížeč nebo terminál zamrzne. Každý prohlížeč, dokonce i jeho různé verze, má jiný limit velikosti zásobníku volání. V naprosté většině případů jsou dostatečné a problém je třeba hledat jinde.
Blokovaný zásobník volání
Zde je příklad blokování vlákna JS. Zkusme přečíst foo a soubor bar pomocí Uzelsynchronní funkce .js readFileSync.
Jedná se o zacyklený GIF. Jak vidíte, JS engine čeká na první volání v položce readFileSync je dokončena. To se však nestane, protože neexistuje žádný foo takže druhá funkce nebude nikdy zavolána.
Asynchronní chování
JS však může být i neblokující a chovat se, jako by byl vícevláknový. To znamená, že nečeká na odezvu volání API, na události I/O atd. a může pokračovat ve vykonávání kódu. Je to možné díky motorům JS, které používají (pod kapotou) skutečné vícevláknové jazyky, jako je C++ (Chrome) nebo Rust (Firefox). Ty poskytují nás s webovým rozhraním API pod kapotou prohlížeče nebo např. I/O API pod Node.js.
Na obrázku GIF výše vidíme, že první funkce je přesunuta na zásobník volání a. Ahoj se okamžitě provede v konzole.
Pak zavoláme setTimeout funkce poskytovaná rozhraním WebAPI prohlížeče. Přejde do zásobníku volání a jeho asynchronní zpětné volání foo funkce přejde do fronty WebApi, kde čeká na volání, které je nastaveno na 3 sekundy.
Mezitím program pokračuje v kódu a my vidíme. Ahoj. Nejsem zablokovaný v konzole.
Po vyvolání přejde každá funkce ve frontě WebAPI do fronty. Fronta zpětných volání. V něm funkce čekají, dokud není zásobník volání prázdný. Když se tak stane, jsou tam přesunuty jedna po druhé.
Takže když naše setTimeout časovač dokončí odpočítávání, naše foo funkce přejde do fronty zpětných volání, počká, až se zpřístupní zásobník volání, přejde tam, provede se a my vidíme Ahoj z asynchronního zpětného volání v konzole.
Smyčka událostí
Otázkou je, jak runtime pozná, že je zásobník volání prázdný, a jak je vyvolána událost ve frontě zpětných volání? Seznamte se se smyčkou událostí. Je součástí jádra JS. Tento proces neustále kontroluje, zda je zásobník volání prázdný, a pokud ano, sleduje, zda ve frontě zpětných volání není událost, která čeká na vyvolání.
To jsou všechna kouzla v zákulisí!
Závěr teorie
Souběžnost a paralelismus
Současnost znamená provádění více úloh najednou, ale ne současně. Např. dvě úlohy pracují v překrývajících se časových úsecích.
Paralelismus znamená provádění dvou nebo více úloh současně, např. provádění více výpočtů najednou.
Vlákna a procesy
Vlákna jsou posloupností provádění kódu, které lze provádět nezávisle na sobě.
Proces je instance běžícího programu. Program může mít více procesů.
Synchronní a asynchronní systém
Na adrese synchronní programování se úlohy provádějí jedna po druhé. Každá úloha čeká na dokončení předchozí úlohy a teprve poté je provedena.
Na adrese asynchronní programování, když je jedna úloha provedena, můžete přejít na jinou úlohu, aniž byste čekali na dokončení té předchozí.
Synchronní a asynchronní v jednovláknovém a vícevláknovém prostředí
Synchronní s jedním vláknem: Úlohy se provádějí jedna po druhé. Každá úloha čeká na provedení předchozí úlohy.
Synchronní s více vlákny: Úlohy jsou prováděny v různých vláknech, ale čekají na další prováděné úlohy v jiném vlákně.
Asynchronní s jedním vláknem: Úlohy se začnou provádět bez čekání na dokončení jiné úlohy. V daném okamžiku může být provedena pouze jedna úloha.
Asynchronní s více vlákny: Úlohy se provádějí v různých vláknech, aniž by čekaly na dokončení jiných úloh, a dokončují se nezávisle.
Klasifikace JavaScript
Pokud se podíváme na to, jak JS engine funguje pod kapotou, můžeme JS klasifikovat jako asynchronní a jednovláknový interpretovaný jazyk. Slovo "interpretovaný" je velmi důležité, protože znamená, že jazyk bude vždy závislý na době běhu a nikdy nebude tak rychlý jako kompilované jazyky s integrovaným vícevláknovým řízením.
Za zmínku stojí, že Node.js může dosáhnout skutečného vícevláknového zpracování za předpokladu, že každé vlákno je spuštěno jako samostatný proces. Existují pro to knihovny, ale Node.js má vestavěnou funkci tzv. Vlákna pracovníků.
Všechny soubory GIF smyčky událostí pocházejí ze souboru Lupa vytvořenou Philipem Robertsem, kde můžete testovat asynchronní scénáře.