Najprawdopodobniej po przeczytaniu tytułu tego wpisu pierwszą myślą, która przyszła Ci do głowy było „wielowątkowość” i „JavaScript” razem? To musi być jakiś błąd.
Pocieszę Cię jednak! To nie jest błąd i w niniejszym wpisie opiszę nową, pojawiającą się dopiero w przeglądarkach technologię – Web Workers. Umożliwia ona tworzenie wielowątkowych aplikacji z wykorzystaniem JavaScript. Aby tworzone przez Ciebie strony były jeszcze lepsze!
Czym jest wielowątkowość?
A więc zacznijmy może od samego początku. Jeśli ktokolwiek jeszcze nie wie czym jest wielowątkowość i na czym polega, odsyłam do Wiki.
W skrócie jednak wyjaśnię, iż polega na rozdzielaniu procesów (wątków działania programu) na kilka wątków. Czyli ujmując to jeszcze prościej i bardziej obrazowo: proces 1 oblicza dane dla arkusza kalkulacyjnego, proces 2 liczy trudne zadania z matematyki, proces 3…
Mam nadzieję, że teraz jest już to jasne. Dodam także, że dotychczas w JavaScript można było „działać” tylko w jednym wątku. Oznaczało to, że jeśli przeglądarka wykonywała jakieś trudne, skomplikowane i zajmujące sporo czasu zadanie, wtedy użytkownikowi końcowemu okienko przeglądarki czasami się blokowało – uniemożliwiało wykonywanie innych procesów, przed ukończeniem obecnego.
Kolejną wadą braku wielowątkowości było brak wsparcia dla następującej sytuacji: nasz użytkownik ma świetny i nowoczesny komputer (64-rdzeniowy). Odpala Twoją stronę i mimo wszystko podczas wykonywania skomplikowanych zadań okienko przeglądarki potrafi się blokować. Dlaczego tak się dzieje? Ano wynika to z faktu, iż JavaScript operuje tylko na jednym wątku, a więc operacje są wykonywane tylko przez jeden rdzeń procesora!
Czym jest Web Workers?
Czas przejść do konkretów i meritum tematu. Web Workers jest to technologia wątków roboczych zaproponowana przez WHATWG (Web Hypertext Application Technology Working Group).
Umożliwia ona wykonywanie kilka różnych wątków przez aplikacje napisane w JavaScript w tym samym czasie! Czyli mamy stronę, na niej mnóstwo kodu JavaScript wykonującego się wraz z określonymi zdarzeniami (to jest nasz wątek główny) i do tego możemy dodawać wątki poboczne, które w normalnych warunkach mogłyby wykonywać się w dość długim czasie.
Przykładowy kod aplikacji
Napiszmy bardzo prosty kod z wykorzystaniem Web Workers. Będzie to kalkulator, który w oddzielnym wątku obliczy nam podatek VAT dla wielu produktów dla faktury zbiorczej :-)
Proszę tylko nie pisać, że nie warto wydzielać do takiego zadania osobnego wątku – dobrze to wiem. Staram się jednak tutaj przedstawić zasadę działania tej technologii (w prawdziwym przypadku wątek mógłby posłużyć do obliczania skomplikowanych arkuszy kalkulacyjnych lub do robienia jeszcze bardziej czasochłonnych zadań).
Kod główny aplikacji:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | (function() { var calc = new Worker("calc.js"); calc.postMessage([ 6458, 4365, 2145, 5348, 55487, 5325, 643, 646, 5235, 64, 435, 346, 20460, 6464, 2324, 787, 807, 4322, 689, 3245, 6865, 53252, 6425, 1234, 421, 467, 334, 674, 45877, 5752, 538, 232, 12234, 4244, 573, 12345, 325 ]); calc.onmessage = function(event) { alert('Cena (+ VAT 22%): ' + event.data.result); } })(window.onload); |
I co tutaj się dzieje? Po kolei:
- Wywołujemy nasz skrypt po wejściu na stronę, dlatego zastosowałem tutaj konstrukcję:
1(function() { ... })(window.onload);
- Tworzymy nowy obiekt Worker, który jest odpowiedzialny za wątki robocze. Jako argument podajemy ścieżkę do pliku, do którego się odwołujemy. Obiekt ten musi być wbudowany w przeglądarkę, o czym dalej.
- Wywołujemy na obiekcie metodę postMessage, która pozwala przesyłać dane pomiędzy procesami (w tym przypadku załączamy tablicę wartości z przypadkowymi cenami).
- Proces jest przekazany do wątku roboczego.
- Po zakończeniu procesu wywoływane jest zdarzenie onmessage obiektu Worker, dlatego musi przypiąć do niego taką funkcję.
- Wyświetlamy wynik przekazany przez wątek roboczy do argumentu event. Do przesłanej wartości odwołujemy się poprzez event.data.
Tak wygląda kod naszego wątku głównego. Teraz czas zająć się kodem, który jest wykonywany w wątku roboczym:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | var result = 0; function update() { postMessage({ result: Math.round(result * 100) / 100 }); } onmessage = function(event) { for (value in event.data) { result += event.data[value] * 1.22; } update(); }; |
- Tworzymy globalną zmienną result, w której będziemy przechowywali wynik.
- Tworzymy funkcję update, która przesyła do wątku głównego odpowiedź za pośrednictwem funkcji postMessage. W tym przypadku dane przesyłamy jako obiekt w notacji JSON.
- Tworzymy funkcję obsługującą zdarzenie onmessage. Czyli nasz wątek roboczy po otrzymaniu wiadomości od wątku głównego wywołuje zdarzenie onmessage (podobnie jak po kliknięciu wywoływane jest onclick), co jest logiczne.
W funkcji jako argument podajemy event, do którego podobnie jak wcześniej dostajemy się poprzez event.data. Jako, że z wątku głównego przesłaliśmy wiadomość w postaci tablicy, teraz przechodzimy przez tą tablicę i liczymy dla każdej liczby podatek 22%.
Na koniec wywołujemy funkcję update, która przesyła wiadomość do wątku głównego.
Myślę, że wszystko jest jasne i nie ma żadnych problemów ze zrozumieniem działania Web Workers.
Web Worker i importScripts
Funkcja importScripts jest składnikiem Web Worker i służy do wczytywania kolejnych plików JavaScript bezpośrednio do naszych wątków roboczych.
Rozszerzmy więc nasz poprzedni kod. Najpierw potrzebujemy utworzyć dwa przykładowe pliki:
functions.js
1 2 3 | var taxRound = function (tax) { return Math.round(tax * 100) / 100; }; |
constants.js
1 | var taxValue = 1.22; |
Teraz zmieniamy główny kod aplikacji, wykorzystując stworzoną zmienną i funkcję, wczytaną przez funkcję importScripts.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | importScripts("constants.js", "functions.js"); var result = 0; function update() { postMessage({ result: taxRound(result) }); } onmessage = function(event) { for (value in event.data) { result += event.data[value] * taxValue; } update(); }; |
I gotowe. Dodana funkcja pobiera kolejno żądane pliki JavaScript, wstrzymując działanie wątku roboczego do zakończenia pobierania, po czym rozpoczyna wykonywanie kodu z użyciem wczytanych funkcjonalności.
Mimo iż w normalnych warunkach wczytanie zewnętrznych plików JavaScript przez dynamiczne tworzenie znaczników nie stanowi żadnych problemów, przy wątkach roboczych jest to niewykonywalne.
Brak współdzielenia przez Web Workers
Wątki robocze nie dzielą żadnych stanów ze stroną, czyli nie mogą manipulować drzewem DOM dokumentu, ingerować w kod dokumentu wywołującego wątek roboczy czy też komunikować się z innymi wątkami roboczymi!
Jedyną możliwością przesłania jakichkolwiek danych do wątku głównego jest użycie funkcji postMessage.
Rozwiązano to w taki sposób, aby nasze wątki robocze nie powodowały problemów z integralnością danych na stronie. Czyli nie musimy obawiać się sytuacji typu: jeden wątek dodał gałąź do dokumentu DOM, drugi wątek usunął tą gałąź, natomiast główny wątek aplikacji chciałby się odwołać do tej, już nieistniejącej gałęzi. I co? Aplikacja by się wysypała.
Dlatego też w wątkach roboczych nie możemy wykorzystać DOM do tworzenia dynamicznie wczytywanych skryptów, lecz musimy wykorzystać funkcję importScripts.
Obsługa błędów w WebWorkers
A co jeśli jakieś zadanie zostanie niewykonane z przyczyn losowych? Sposób można rozwiązać w wątku roboczym – zapisać wiadomość w logach dla administratora, by ten wiedział co i gdzie poprawiać.
Można także dodać funkcję obsługi błędów do wątku głównego – jeśli przez dłuższy czas wątek roboczy nie będzie dawał oznak życia to znaczyć będzie, że należy powiadomić użytkownika o zaistniałym problemie i przerwać wykonywanie wątku roboczego.
Robimy to w następujący sposób (rozszerzenie poprzedniego przykładu):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | (function() { var calc = new Worker("calc.js"); calc.postMessage([ 6458, 4365, 2145, 5348, 55487, 5325, 643, 646, 5235, 64, 435, 346, 20460, 6464, 2324, 787, 807, 4322, 689, 3245, 6865, 53252, 6425, 1234, 421, 467, 334, 674, 45877, 5752, 538, 232, 12234, 4244, 573, 12345, 325 ]); calc.onmessage = function(event) { alert('Cena (+ VAT 22%): ' + event.data.result); } calc.onerror = function(event) { alert( 'Plik z błędem: ' + event.filename + '\n' + 'Linia: ' + event.lineno + '\n' + 'Komunikat: ' + event.message ); } })(window.onload); |
Wystarczy jeszcze wprowadzić jakiś błąd w pliku calc.js (np. niekończąca się pętla) i naniesione poprawki będziemy mogli sprawdzić w praktyce.
Wsparcie przeglądarek dla Web Workers
Web Workers jest wchodzącym w życie standardem i składnikiem HTML5. Przeglądarki zapowiedziały wprowadzanie funkcjonalności HTML5 w życie, więc wkrótce z całą pewnością niniejsze wątki będzie można wprowadzać w prawdziwych aplikacjach!
Obecnie jednak Web Workers nie jest obsługiwany przez wszystkie przeglądarki (oczywiście nie przez IE). Poniżej spis wersji przeglądarek obsługujących wątki robocze:
- Chrome 4.0
- Mozilla 3.5
- Opera 10.6
- Safari 4.0
A co z przeglądarkami nieobsługującymi Web Workers? Strona się wysypie, technologia nie zadziała i koniec zabawy?
Na szczęście z pomocą przychodzi świetny skrypt autorstwa KillerKiwi2005, który w zupełności tutaj wystarczy: Internet Explorer Worker Thread Emulation.
Podsumowanie
Jak widać, ta technologia jest naprawdę świetna. Ponadto obsługiwana już przez wszystkie najnowsze przeglądarki. W dodatku zapewnione jest wsparcie dla przeglądarek nieobsługujących Web Workers (nie jest to idealne rozwiązanie, lecz powinno wystarczyć). Nic tylko używać!
Choć nigdy wcześniej nie używałem Web Workers w prawdziwej aplikacji, zamierzam w najbliższym czasie to zmienić. W razie większych sukcesów lub niepowodzeń opiszę swoje doświadczenia na blogu.
Także innych zapraszam do wykorzystywania nowych technologii i dzielenia się zdobytą wiedzą, także i ze mną w komentarzach :-)
Brzmi ciekawie, choć brakuje mi kilku rzeczy, np.: synchronizacji wątków, określania sposobu podziału zadań i sposobu zrównoleżniania…
Z drugiej strony – wg mnie – to powinno się dziać natywnie – szczególnie ‚nie przycinanie przeglądarki’.
Coś takiego to się wywoła od razu a nie onload.
@Jakub Jankiewicz: masz rację ;) to stare dzieje, musiałem przez niewiedzę lub pomyłkę ominąć.