JavaScript jest językiem jednowątkowym, a jednocześnie nieblokującym, asynchronicznym i współbieżnym. Ten artykuł wyjaśni ci, jak to się dzieje.
Czas działania
JavaScript jest językiem interpretowanym, a nie kompilowanym. Oznacza to, że potrzebuje on interpretera, który konwertuje JS kod do kodu maszynowego. Istnieje kilka rodzajów interpreterów (zwanych silnikami). Najpopularniejsze silniki przeglądarek to V8 (Chrome), Quantum (Firefox) i WebKit (Safari). Nawiasem mówiąc, V8 jest również używany w popularnym środowisku uruchomieniowym innym niż przeglądarka, Node.js.
Każdy silnik zawiera stertę pamięci, stos wywołań, pętlę zdarzeń, kolejkę wywołań zwrotnych i interfejs WebAPI z żądaniami HTTP, licznikami czasu, zdarzeniami itp., wszystkie zaimplementowane na swój własny sposób w celu szybszej i bezpieczniejszej interpretacji kodu JS.
Podstawowa architektura środowiska uruchomieniowego JS. Autor: Alex Zlatkov
Pojedynczy wątek
Język jednowątkowy to język z pojedynczym stosem wywołań i pojedynczą stertą pamięci. Oznacza to, że w danym momencie wykonywana jest tylko jedna operacja.
A stos jest ciągłym regionem pamięci, alokującym lokalny kontekst dla każdej wykonywanej funkcji.
A sterta to znacznie większy region, przechowujący wszystko, co zostało przydzielone dynamicznie.
A stos wywołań to struktura danych, która zasadniczo rejestruje, gdzie jesteśmy w programie.
Stos wywołań
Napiszmy prosty kod i prześledźmy, co dzieje się na stosie wywołań.
Jak widać, funkcje są dodawane do stosu, wykonywane, a następnie usuwane. Jest to tak zwany sposób LIFO - Last In, First Out. Każdy wpis na stosie wywołań jest nazywany ramka stosu.
Znajomość stosu wywołań jest przydatna do odczytywania śladów stosu błędów. Ogólnie rzecz biorąc, dokładna przyczyna błędu znajduje się na górze w pierwszej linii, chociaż kolejność wykonywania kodu jest oddolna.
Czasami można poradzić sobie z popularnym błędem, zgłaszanym przez Przekroczono maksymalny rozmiar stosu wywołań. Można to łatwo uzyskać za pomocą rekurencji:
function foo() {
foo()
}
foo()
i nasza przeglądarka lub terminal zawiesza się. Każda przeglądarka, nawet w różnych wersjach, ma inny limit wielkości stosu wywołań. W zdecydowanej większości przypadków są one wystarczające i problemu należy szukać gdzie indziej.
Zablokowany stos wywołań
Oto przykład blokowania wątku JS. Spróbujmy odczytać plik foo i plik pasek przy użyciu WęzełFunkcja synchroniczna .js readFileSync.
To jest zapętlony GIF. Jak widać, silnik JS czeka do pierwszego wywołania w pliku readFileSync jest zakończona. Ale tak się nie stanie, ponieważ nie ma foo więc druga funkcja nigdy nie zostanie wywołana.
Zachowanie asynchroniczne
Jednak JS może być również nieblokujący i zachowywać się tak, jakby był wielowątkowy. Oznacza to, że nie czeka na odpowiedź wywołania API, zdarzenia I/O itp. i może kontynuować wykonywanie kodu. Jest to możliwe dzięki silnikom JS, które wykorzystują (pod maską) prawdziwe języki wielowątkowe, takie jak C++ (Chrome) lub Rust (Firefox). Zapewniają nam one Web API pod maską przeglądarki lub np. I/O API pod Node.js.
Na powyższym GIF-ie widzimy, że pierwsza funkcja jest wypychana na stos wywołań i Cześć jest natychmiast wykonywana w konsoli.
Następnie wywołujemy setTimeout dostarczana przez WebAPI przeglądarki. Przechodzi ona do stosu wywołań i jej asynchronicznego wywołania zwrotnego foo funkcja przechodzi do kolejki WebApi, gdzie czeka na połączenie, które ma nastąpić po 3 sekundach.
W międzyczasie program kontynuuje kod i widzimy Cześć, nie jestem zablokowany w konsoli.
Po wywołaniu, każda funkcja w kolejce WebAPI trafia do pliku Kolejka wywołania zwrotnego. Jest to miejsce, w którym funkcje czekają, aż stos wywołań będzie pusty. Gdy tak się stanie, są tam przenoszone jedna po drugiej.
Tak więc, kiedy nasz setTimeout timer zakończy odliczanie, nasz foo funkcja przechodzi do kolejki wywołania zwrotnego, czeka, aż stos wywołań stanie się dostępny, przechodzi tam, jest wykonywana i widzimy Cześć z asynchronicznego wywołania zwrotnego w konsoli.
Pętla zdarzeń
Pytanie brzmi, skąd środowisko wykonawcze wie, że stos wywołań jest pusty i jak wywoływane jest zdarzenie w kolejce wywołań zwrotnych? Poznaj pętlę zdarzeń. Jest ona częścią silnika JS. Proces ten stale sprawdza, czy stos wywołań jest pusty, a jeśli tak, monitoruje, czy w kolejce wywołań zwrotnych znajduje się zdarzenie oczekujące na wywołanie.
To cała magia za kulisami!
Podsumowując teorię
Współbieżność i równoległość
Współbieżność oznacza wykonywanie wielu zadań w tym samym czasie, ale nie jednocześnie. Np. dwa zadania działają w nakładających się okresach czasu.
Równoległość oznacza wykonywanie dwóch lub więcej zadań jednocześnie, np. wykonywanie wielu obliczeń w tym samym czasie.
Wątki i procesy
Nici są sekwencją wykonywania kodu, które mogą być wykonywane niezależnie od siebie.
Proces jest instancją uruchomionego programu. Program może mieć wiele procesów.
Synchroniczne i asynchroniczne
W synchroniczny W programowaniu zadania są wykonywane jedno po drugim. Każde zadanie czeka na zakończenie poprzedniego i dopiero wtedy jest wykonywane.
W asynchroniczny programowanie, gdy jedno zadanie jest wykonywane, można przełączyć się na inne zadanie bez czekania na zakończenie poprzedniego.
Synchroniczne i asynchroniczne w środowisku jedno- i wielowątkowym
Synchroniczne z pojedynczym wątkiem: Zadania są wykonywane jedno po drugim. Każde zadanie czeka na wykonanie poprzedniego zadania.
Synchroniczny z wieloma wątkami: Zadania są wykonywane w różnych wątkach, ale czekają na inne zadania wykonywane w innym wątku.
Asynchroniczny z pojedynczym wątkiem: Zadania zaczynają być wykonywane bez oczekiwania na zakończenie innego zadania. W danym momencie może być wykonywane tylko jedno zadanie.
Asynchroniczny z wieloma wątkami: Zadania są wykonywane w różnych wątkach bez oczekiwania na zakończenie innych zadań i kończą swoje wykonywanie niezależnie.
Klasyfikacja JavaScript
Jeśli weźmiemy pod uwagę, jak silniki JS działają pod maską, możemy sklasyfikować JS jako asynchroniczny i jednowątkowy język interpretowany. Słowo "interpretowany" jest bardzo ważne, ponieważ oznacza, że język zawsze będzie zależny od czasu wykonywania i nigdy nie będzie tak szybki jak języki kompilowane z wbudowaną wielowątkowością.
Warto zauważyć, że Node.js może osiągnąć prawdziwą wielowątkowość, pod warunkiem, że każdy wątek jest uruchamiany jako oddzielny proces. Istnieją do tego biblioteki, ale Node.js ma wbudowaną funkcję o nazwie Wątki pracownicze.
Wszystkie GIF-y pętli zdarzeń pochodzą z Lupa stworzona przez Philipa Robertsa, w której można testować asynchroniczne scenariusze.