Problem ze zrozumienie asynchroniczności na przykładzie setTimeout()

0

Witam.

Nie rozumiem pewnej rzeczy związanej z asynchronicznością i wykorzystaniem funkcji setTimeout. Z tego co się dowiedziałem funkcja ta pozwala na wykonanie pewnego kodu po określonym czasie. Dodatkowo jest to funkcja asynchroniczna, czyli, może być wykonywana poza głównym wątkiem, przykładowy kod:

console.log(1);
setTimeout(() => {
    console.log(2);
}, 0)
console.log(3);

Skrypt wyświetli cyfry w kolejności 1, 3, 2, nawet mimo, że funkcja setTimeout drugi argument ma ustawiony na 0 milisekund, gdyż jest to funkcja asynchroniczna, czyli wykonywana poza głównym wątkiem.

Tylko do tej pory myślałem, że bez względu na to, co się będzie dziać w setTimeout, to nie będzie to blokowało działania strony. Zmodyfikowałem nieco powyższy skrypt:

console.log(1);
setTimeout(() => {
    for(let i = 0; i < 3000000000; i++){ 
    }
    console.log(2);
}, 0)
console.log(3);

Następnie dodałem go do pliku .html w którym zrobiłem pole textarea, żeby móc coś tam wpisać i po uruchomieniu strony okazało się, że jednak strona się "zlagowała", nie mogłem nic wpisać w polu tekstowym i musiałem poczekać, aż skończy się pętla w setTimeout.

I nie potrafię tego zrozumieć, dlaczego strona się zacięła, skoro setTimeout jest niby funkcją asynchroniczną i podobno nie może zablokować strony, w sensie operacje w funkcjach asynchronicznych wykonują się w tle i taka jest idea, żeby nie blokowały działania strony. Ok, console.log(3) wykonało się przed setTimeout, jednak ta pętla miała "zasymulować" odczyt dużego pliku lub pobieranie danych z API (a właśnie taka operacja może podobno zaciąć stronę, gdyby cały kod był synchroniczny) i mimo że była w funkcji asynchronicznej, to i tak "zlagowała" stronę.

Czy może mi ktoś wytłumaczyć dlaczego tak się stało?

4

setTimeout nie jest asynchroniczne, wszystko się dzieje w głównym wątku tylko odkłada wykonanie do następnego cyklu wydarzeń.
Poczytaj na necie jest pełno materiałów.
Jak chcesz asynchronicznej funkcji to masz web workery

0

To ciekawe, bo już trochę czytam o tej całej asynchroniczności w JS i myślałem, że to powinno zadziałać.

Czyli mam rozumieć, że nawet jakbym wrzucił ten kod w Promise'a lub w funkcję z wykorzystaniem async await, to i tak zlagowałoby mi stronę, i jedyny efekt jaki bym uzyskał, to że console.log(3) po prostu wykonałoby się w pierwszej kolejności, zamiast console.log(2)?

3

klasyka, gdzie jest to dobrze wytłumaczone:

I nie potrafię tego zrozumieć, dlaczego strona się zacięła, skoro setTimeout jest niby funkcją asynchroniczną i podobno nie może zablokować strony, w sensie operacje w funkcjach asynchronicznych wykonują się w tle i taka jest idea, żeby nie blokowały działania strony.

Wręcz przeciwnie. W JS ponieważ domyślnie rzeczy się odpalają w jednym wątku, to trzeba uważać, żeby kod nie robił zbyt dużych obliczeń w jednej iteracji, ponieważ w JS każdy taki kod blokuje wątek. Owszem, jest idea, żeby nie blokowały działania strony. ale to twoja odpowiedzialność jest, żeby napisać non-blocking code (czyli coś, co się odpali jak najszybciej i zwróci sterowanie).

Z tego powodu przez lata asynchroniczne pisanie w JS to było "callback hell" (bo nie mogłeś blokować nic długo, więc wysyłałeś np. żądanie do serwera i potem callback, żeby odebrać, potem kolejną asynchroniczną akcję i kolejny zagnieżdżony callback), potem wymyślono promisy, a potem async/await i można pisać asynchronicznie tak, jakby się pisało synchroniczny kod, nawet jeśli pod spodem będzie on ciągle się zatrzymywał i czekał na kolejny event.

2

Tak, wszystko w javascript się dzieje w jednym wątku. prawdziwie asynchroniczne mogą być tylko niektóre operacje jak np fetch, I/O i web workery ale obsługa ich rezultatów wraca zawsze do tego samego wątku. Dzięki temu język jest bardzo prosty i nie trzeba się bawić w sekcje krytyczne i synchronizacje

a setTimeout służy do odpalenia kodu w przyszłości, podanie zerowego czasu to tylko taki hack żeby odpalić ten kod po wszystkim innym tak szybko jak się da. Można też użyć queueMicrotask a w node process.nextTick, różnica taka że mikrotaski odpalają się jeden za drugim do wyczerpania, więc z kodu wewnątrz setTimeout możesz wywołać znowu setTimeout, ale queueMicrotask może stworzyć nieskończoną pętlę

2

rzecz w tym, że w kontekście JSa asynchroniczność może dziać się w jednym wątku i domyślnie tak jest. Może kwestia nazewnictwa, może w innych językach słowo asynchroniczny oznacza wielowątkowy, ale w JS jak się mówi o asynchronous code, to równie dobrze można mówić o tym jednowątkowym kodzie.

0

@LukeJL: Tylko właśnie problem jest taki, że najwyraźniej coś źle zrozumiałem z tej asynchroniczności i myślałem, że jak wstawię taką pętle do setTimeout (lub do Promise'a) to będzie się to elegancko wykonywać w tle, ale teraz już widzę, że jedyny efekt jaki uzyskałem, to że console.log(3) zdążyło się wykonać przed console.log(2).

Czyli w sumie reszta kodu faktycznie wykonała się PRZED setTimeout (wyświetlenie 1, 3, 2 w konsoli), jednak wychodzi na to, że kod znajdujący się w środku setTimeout i tak może przyciąć działanie strony.

@obscurity:

1
kario97 napisał(a):

Czyli w sumie reszta kodu faktycznie wykonała się PRZED setTimeout (wyświetlenie 1, 3, 2 w konsoli), jednak wychodzi na to, że kod znajdujący się w środku setTimeout i tak może przyciąć działanie strony.

No to klasyczne pytanie rekrutacyjne sprawdzające, czy kandydat rozumie asynchroniczność, że robi się taki przykład i który console.log się wykona kiedy (jeszcze Promise.resolve() się tam dokłada:

Promise.resolve().then(() => console.log(4));
console.log(1);
setTimeout(() => {
    console.log(2);
}, 0)
console.log(3);

bo to wszystko jest logiczne.

  • wywoła się Promise.resolve(), ale nie ta funkcja, którą masz w then, tylko zarejestrujesz swój callback
  • console.log 1
  • zarejestrujesz swoją funkcję przez setTimeout(która się nie wywoła jeszcze)
  • console.log 3
  • potem kod się kończy i zwraca sterowanie. Odpali się funkcja w then, więc console.log 4
  • potem odpali się timeout, więc console.log 2
0

@LukeJL: Ty i kolega @obscurity: pisaliście o "odkładaniu" zadań do następnego cyklu zdarzeń. Odpaliłem Twój kod i faktycznie konsola wypisała 1, 3, 4, 2. Czyli mam rozumieć, że w następnym cyklu instrukcje są wykonywane w kolejności, w jakiej zostały odłożone (bo najpierw wyświetliło 4, potem 2)? Czyli tutaj działa tzw zasada FIFO (first in, first out)?

2
kario97 napisał(a):

Czyli mam rozumieć, że w następnym cyklu instrukcje są wykonywane w kolejności, w jakiej zostały odłożone (bo najpierw wyświetliło 4, potem 2)? Czyli tutaj działa tzw zasada FIFO (first in, first out)?

nie polegałbym na tym, być może tak jest natomiast nie widzę wzmianki w standardzie i każda implementacja może być różna

LukeJL napisał(a):

No to klasyczne pytanie rekrutacyjne sprawdzające, czy kandydat rozumie asynchroniczność, że robi się taki przykład i który console.log się wykona kiedy

trochę głupie zadanie, niech jeszcze dorzucą requestAnimationFrame i queueMicrotask. Czy wiedza które z setTimeout vs then odpali się pierwsze jest taka ważna? W prawdziwym życiu nie da się przewidzieć co wyjdzie pierwsze bo Promise zapewne będzie zawierał jakieś wywołanie asynchroniczne, zadanie nie sprawdza czy ktoś rozumie asynchroniczność tylko czy już odpalał sobie taki kod

// edit:
znalazłem odniesienie w dokumentacji
https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model

Z tego wychodzi że taski powinny być odpalane w zaplanowanej kolejności o ile pochodzą z pustego lub pełni aktywnego dokumentu. Czyli jeżeli masz na przykład dwa iframe'y na stronie i w każdym na początku jest setTimeout 0 to pierwszy wykona się ten który pochodzi z dokumentu który się załaduje wcześniej a nie ten który został zaplanowany wcześniej

1
kario97 napisał(a):

@LukeJL: Ty i kolega @obscurity: pisaliście o "odkładaniu" zadań do następnego cyklu zdarzeń. Odpaliłem Twój kod i faktycznie konsola wypisała 1, 3, 4, 2. Czyli mam rozumieć, że w następnym cyklu instrukcje są wykonywane w kolejności, w jakiej zostały odłożone (bo najpierw wyświetliło 4, potem 2)? Czyli tutaj działa tzw zasada FIFO (first in, first out)?

Nie o to tutaj chodzi. To promise resolve możesz dać na końcu i dalej będzie przed timeoutem. Bo to microtaski. Czyli setTimeout vs. Promise.resolve.

1

Pętla Ci zlagowała stronę, nie setTimeout i asynchroniczność. Ustawiłeś gigantyczną pętlę, żeby wykonała się po x milisekundach. Więc odkładasz w tym przypadku turbo-laga o x milisekund. Jeśli chcesz, żeby pętla lub coś podobnego wykonywała się powoli, to możesz użyć setInterval. Tylko wtedy musisz ręcznie wpisać co ile milisekund będzie się wykonywała jedna iteracja.

1

Tu jest o mikrotaskach
https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide
one się odpalają jeszcze zanim apka powróci do pętli zdarzeń. Dlatego w tamtym przypadku console.log w promisie wywołało się przed console.log w timeoucie.

0

Hej @LukeJL: mam jeszcze dwa ostatnie pytania:

  1. Czyli mam rozumieć, że tak jak napisałeś (i kolega @obscurity: ) w JS wszystko dzieje się w jednym wątku, nawet rzeczy asynchroniczne takie jak funkcja setTimeout() czy Promisy. Po prostu ta cała asynchroniczność w JS polega na tym, że pozwala odłożyć wykonanie pewnych operacji na później, ale i tak wątek jest jeden. Ja po prostu pomyliłem pojęcia i myślałem, że asynchroniczność w JS to de facto wielowątkowość z innych języków programowania. Zresztą sam napisałeś wyżej, że

Może kwestia nazewnictwa, może w innych językach słowo asynchroniczny oznacza wielowątkowy ale w JS jak się mówi o asynchronous code, to równie dobrze można mówić o tym jednowątkowym kodzie.

  1. Napisałeś o microtaskach i podałeś link do dokumentacji. Mam pytanie, czy ja już teraz powinienem to wszystko przeczytać i się tego nauczyć, w sensie czy konieczne jest na tę chwilę, abym po przeczytaniu kodu był w stanie stwierdzić z całą pewnością, w jakiej kolejności co się wykona? Czy na początku nie powinienem sobie tym zbytnio zaprzątać głowy?
1
kario97 napisał(a):

Hej @LukeJL: mam jeszcze dwa ostatnie pytania:

  1. Czyli mam rozumieć, że tak jak napisałeś (i kolega @obscurity: ) w JS wszystko dzieje się w jednym wątku,

Są już dostępne wątki w przeglądarce czy w Node.js, więc jeśli chcesz odpalić wątek, masz taką możliwość
https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
https://nodejs.org/api/worker_threads.html
Ale domyślnie wszystko się dzieje w jednym wątku i najczęstszą metodą pracy jest praca w jednym wątku i pisanie w tym jednym wątku kodu asynchronicznego i nie blokującego (nawet w konsoli błędów w przeglądarce może pojawić się ostrzeżenie, jak zbyt długo zajmie obsługa jakiegoś handlera zdarzeń).

  1. Napisałeś o microtaskach i podałeś link do dokumentacji. Mam pytanie, czy ja już teraz powinienem to wszystko przeczytać i się tego nauczyć, w sensie czy konieczne jest na tę chwilę, abym po przeczytaniu kodu był w stanie stwierdzić z całą pewnością, w jakiej kolejności co się wykona? Czy na początku nie powinienem sobie tym zbytnio zaprzątać głowy?

Myślę, że pośrednie rozwiązanie będzie najlepsze.
Czyli uczyć się na tyle, ile jesteś w stanie zrozumieć/przyswoić w danym czasie. Porobić ćwiczenia sprawdzające, czy faktycznie jesteś w stanie przewidzieć, jak się zachowa program. A potem najwyżej wrócić do tematu po jakimś czasie. To nie jest tak, że jak się uczysz języka programowania czy jakiejś technologii, to możesz się nauczyć od razu jakiegoś tematu w 100%. Bo może być tak, że go zaledwie liźniesz za pierwszym razem. I to też jest okej.

1

Niezrozumienie wynika z tego, że one są wertykalnie w kodzie.

console.log(1);
setTimeout(() => {
    console.log(2);
}, 0)
console.log(3);

Wystarczy łatwo przerobić ten kod, nie zmieniając jego zachowania:

function do_stuff() {
  console.log(2);
}

console.log(1);
setTimeout(method, 0)
console.log(3);

To co się wykona to tak:

  1. Zadeklarowanie funkcji do_stuff(), bez wywołania
  2. Wywołanie console.log(1);
  3. Wywołanie setTimeout(), który odłoży metodę method na koniec stosu wywołań (nie wywoła jej od razu)
  4. Wywołanie console.log(3);
  5. Nie ma więcej linijek
  6. Wykonanie metod odłożonych na stosie -Jedyne takie wywołanie to jest call do method, więc zostaje wywołane.
kario97 napisał(a):
  1. Czyli mam rozumieć, że tak jak napisałeś (i kolega @obscurity: ) w JS wszystko dzieje się w jednym wątku, nawet rzeczy asynchroniczne takie jak funkcja setTimeout() czy Promisy. Po prostu ta cała asynchroniczność w JS polega na tym, że pozwala odłożyć wykonanie pewnych operacji na później, ale i tak wątek jest jeden.

Dokładnie tak.

kario97 napisał(a):

Ja po prostu pomyliłem pojęcia i myślałem, że asynchroniczność w JS to de facto wielowątkowość z innych języków programowania. Zresztą sam napisałeś wyżej, że

Dlatego są na to inne słowa, bo to są inne rzeczy. Podobnie działa async w Pythonie, i z tego co wiem to w C# też (chyba Task z C# jest jakimś odpowiednikiem Promise?).

1

Tak jak ludzie mówili to jest też mocno zależne od platformy. Asynchroniczność na froncie fajnie rozwiązuje Angular przez RxJS. Mi zawsze się przyjemniej pracuje z Observable, aniżeli z Promise, bo operują na wyższym poziomie abstrakcji.

W .NET masz taki typ jak Task, który jest wrapperem odnośnie jednego wątku, a z racji, że wątków jest cała pula to serwer będzie w stanie przetworzyć w jednym momencie więcej tematu. 😉

W JS są dwie kolejki: mikrotasków i makrotasków i stąd wynika takie zachowanie. 😊

1 użytkowników online, w tym zalogowanych: 0, gości: 1