Niniejszy post jest ostatnim z serii “Implementacja przepływu aplikacji” będącej jedną z czterech głównych zagadnień egzaminu MCSD: 70-480. Zajmiemy się w nim omówieniem, wprowadzonego w HTML5, API “Web Workers”, pozwalającego na uruchamianie skryptów “w tle”. Dowiemy się więc jak wystartować i jak zakończyć działanie Web Workera; w jaki sposób przekazywać dane do procesu Web Workera i jak ustawić dla niego timeout i interwał; poznamy także sposób na rejestrację obserwatora zdarzenia dla Web Workera, a także poznamy ich ograniczenia. Jak widzisz, wielowątkowy JavaScript nie będzie już dla Ciebie tajemnicą po przeczytaniu tego artykułu…

Zanim przejdziemy do części właściwej dzisiejszego wpisu, trochę definicji. Termin “Web Workers” to nowe, dostępne w HTML5 API, pozwalające programistom na uruchamianie “zasobożernych” części skryptów JavaScript w osobnym wątku, dzięki czemu, podczas działania takiego skryptu, interfejs użytkownika nie jest blokowany.

Tworzenie, uruchamianie i zamykanie wątków

Aby utworzyć nowy wątek należy stworzyć instancję klasy ‘Worker’, tak jak na poniższym przykładzie:

var worker = new Worker('background.js');

Jako parametr konstruktora tej klasy, podajemy adres URL skryptu, który ma zostać wykonany “w tle”. Mając utworzony wątek, możemy go uruchomić poprzez przesłanie do niego wiadomości:

worker.postMessage();

Natomiast aby wymusić zakończenie działania wątku, wystarczy skorzystać z metody ‘terminate’:

worker.terminate();

Komunikacja między wątkiem głównym i roboczym

Komunikacja pomiędzy wątkami, odbywa się za pomocą metody ‘postMessage’ (jej przykład wyżej), którą wykorzystuje się do wysyłania wiadomości z jednego wątku do drugiego, oraz za pomocą obsługi zdarzenia ‘message’, dzięki czemu możemy reagować na otrzymanie wiadomości z innego wątku. Poniższy przykład, pokazuje w jaki sposób realizować można komunikację w obie strony:

var worker = new Worker('background.js');

// rejestracja obsługi zdarzenia 'message' wysłanego przez 'worker'
worker.addEventListener('message', function(e) {
    alert('otrzymano odpowiedź: ' + e.data);
}, false);

// wyslanie wiadomosci start
worker.postMessage('start');

// wyslanie wiadomosci stop
worker.postMessage('stop');

---------------- kod pliku background.js ----------------

// obsluga zdarzenia 'message' wyslanego z watku glownego
this.addEventListener('message', function(e) {
    switch(e.data) {
        case 'start':
            this.postMessage('watek uruchomiony!');
            break;
        case 'stop':
            this.postMessage('watek zatrzymany!');
            this.close(); // zatrzymanie skryptu wewnatrz watku roboczego
            break;
        default:
            this.postMessage('nieznane polecenie!');
    }
}, false);

Omówienie

Na początek tworzona jest nowa instancja klasu ‘Worker’. Następnie, w kontekście nowego obiektu rejestrujemy obsługę zdarzenia ‘message’ (realizujemy to za pomocą metody ‘addEventListener’, w taki sposób jak opisywałem to już w poście na temat obsługi zdarzeń w języku JavaScript). Jak widać, do funkcji obsługującej zdarzenie przekazywany jest parametr wejściowy, zawierający dane przekazane z wątku roboczego - w tym przypadku we właściwości ‘data’ parametru ‘e’ spodziewamy się wartości typu ‘string’ ale równie dobrze mógłby to być też obiekt.

W kolejnych liniach (dziewiąta i dwunasta), wysyłamy do wątku roboczego kolejno wiadomość ‘start’ a następnie ‘stop’ (które myślę, że wiadomo co robią ;)).

Następnie w powyższym przykładzie widzimy implementację, która w normalnych warunkach znalazłaby się w pliku ‘background.js’. Jak widać, cały kod tego skryptu to rejestracja obsługi zdarzenia ‘message’ (tym razem są to wiadomości przesyłane z wątku głównego do wątku roboczego - czyli najpierw przyjdzie wiadomość ‘start’ a następnie ‘stop’). W ciele funkcji obsługującej zdarzenie, mamy instrukcję warunkową, która w zależności od przesłanej z wątku głównego wiadomości, wykonuje odpowiednie polecenia. I tak w przypadku gdy otrzymamy wiadomość ‘start’ (ponownie dane te przekazywane są za pomocą parametru wywołania funkcji obsługującej zdarzenie), wysyłany jest za pomocą metody ‘postMessage’ odpowiednia wiadomość do wątku głównego. Natomiast w przypadku odebrania wiadomości ‘stop’, oprócz wysłania odpowiedniej wiadomości, za pomocą metody ‘close’ zatrzymywany jest bieżący wątek roboczy (a więc z wnętrza wątku roboczego, w celu zamknięcia wątku korzystamy z ‘close’ z poza niego, korzystamy z ‘terminate’).

W razie otrzymania innej niż spodziewana informacji, wysyłana jest wiadomość z odpowiednią informacją.

Oczywiście, do rejestracji zdarzenia nie trzeba koniecznie stosować metody ‘addEventListener’ - sposób z przypisaniem funkcji obsługującej bezpośrednio do właściwości ‘onmessage’ workera, również będzie działać, ale tak jak opisałem parę postów wcześniej, uniemożliwia nam to podpinanie większej ilości funkcji do jednego zdarzenia.

Należy także zwrócić uwagę na użycie słowa kluczowego ‘this’ w skrypcie ‘background.js’ - odnosi się ono do obiektu wątku, dlatego konstrukcja ‘this.postMessage’ powoduje wysłanie wiadomości z odpowiedniego wątku roboczego.

Wątki podrzędne

Ponadto, istnieje możliwość tworzenia “pod-wątków” czyli wątków roboczych, które tworzone są w innych wątkach roboczych. Istnieją jednak pewne ograniczenia dotyczące wątków podrzędnych:

  • Pliki źródłowe wątków podrzędnych muszą być hostowane w tej samej domenie (polityka “same origin”)
  • Ich URL’e ustalane są relatywnie do lokalizacji pliku źródłowego wątku-twórcy a nie oryginalnego dokumentu (o ile podana została ścieżka względna)

Obsługa błędów

Obsługa błędów w tym przypadku opiera się na rejestracji odpowiedniej funkcji obsługującej zdarzenie ‘error’. Spójrzmy na przykład:

var worker = new Worker('background.js');

// rejestracja obsługi zdarzenia 'error' - obsluga bledow
worker.addEventListener('error', function(e) {
    alert('wystapil blad w linii: ' + e.lineno +
          ' w pliku: ' + e.filename + '.' +
          'Tresc bledu: ' + e.message);
}, false);

Jak widać, w funkcji obsługującej zdarzenie ‘error’, poprzez parametr wejściowy ‘e’, mamy dostęp do kilku przydatnych informacji: właściwość ‘lineno’ zawiera informację o numerze linii, w której wystąpił błąd; właściwość ‘filename’, jak sama nazwa wskazuje zawiera informację w jakim pliku wystąpił błąd; właściwość ‘message’ to z kolei treść błędu.

Ograniczenia i informacje dodatkowe

Oprócz tego, w skryptach uruchamianych jako wątki dostępny jest pewien podzbiór funkcji JavaScript. Można więc zatem używać taki metod jak:

  • Obiekt ‘navigator’ - udostępniający informacje o przeglądarce
  • Obiekt ‘location’, tylko do odczytu
  • Obiekt XMLHttpRequest
  • Metody ‘setTimeout/getTimeout’ oraz ‘setInterval/getIntervarl’ pozwalające na uruchamianie kodu wątku z opóźnieniem lub co pewien okres czasu
  • AppCashe API
  • Metoda ‘importScripts’, dzięki której do kodu wątku roboczego, dołączać można dodatkowe skrypty

Nie mamy natomiast dostępu do:

  • Obiektów DOM (ponieważ nie są “thread safe”) - wszystkie operacjie na obiektach DOM powinny być wykonywane w wątku głónym
  • Obiektu ‘window’
  • Obiektu ‘document’
  • Obiektu ‘parent’

Ponadto, wspominałem wcześniej, że jako parametr metody ‘postMessage’, przekazywać można obiekty - jest jednak pewne ograniczenie, ponieważ API Web Workers, podczas przekazywania takiego obiektu pomiędzy wątkami, automatycznie konwertuje taki obiekt do JSON’a. Nie jest więc możliwe przekazywanie obiektów, które na przykład zawierają definicje funkcji.

Wielowątkowy JavaScript - podsumowanie

Na podstawie przytoczonych informacji widać, że wielowątkowy JavaScript, który osiągamy za pomocą API Web Workers to nie jest specjalnie nic trudnego. Myślę, że w dobie coraz bardziej rozbudowanych i “zasobożernych” skryptów zawartych w dzisiejszych stronach internetowych, takie rozwiązanie może być przydatne do rozwiązania niektórych problemów wydajnościowych. Jak zawsze jednak, z programowania wielowątkowego należy korzystać z głową;)

Postem tym kończę omawianie drugiego zagadnienia wymaganego na egzaminie 70-480 czyli “Implementacji przepływu aplikacji” - tym sposobem dobrnęliśmy do półmetka tego cyklu! Zapraszam na kolejne wpisy, w których przejdę do omawiania zagadnień związanych z dostępem i zabezpieczaniem danych.