Thread.js
• tech • 1107 słów • 6 minut czytania
Zgodnie z zapowiedziami, kontynuacja tematu z ostatniej notki, w której przedstawiałem sposoby umożliwiające w pewnym stopniu na emulacje środowiska wielowątkowego w JS. Teraz, jak obiecałem, nadszedł czas na przedstawienie mojej implementacji, prostej biblioteki umożliwiającej w bardzo prosty sposób emulować wielowątkowość.
Wprowadzenie
Wspominałem w poprzedniej notatce, że sam problem zawieszania się i blokowania przeglądarki, przez długo wykonywujący się kod JS mnie za bardzo nie dotyczył - dopóki sam nie musiałem rozwiązać tego problemu. Działo się to kilka lat temu, gdy pracowałem w Gemiusie nad silnikiem renderującym i przetwarzającym dane na stronie.
Zacząłem wtedy mały research w sieci, szukając jakiegoś ciekawego i prostego rozwiązania. Oczywiście najwięcej różnorakich sugestii związanych było z asynchronicznym wykonywaniem małych segmentów kodu, głównie za pomocą opisanych wyżej technik z setTimeout
na czele. Trafiłem również (chyba tutaj) na pewne ciekawe rozwiązanie, wykorzystujące nowe elementy wprowadzone do JS - opisywane wyżej generatory. Była to implementacja wątków Neil Mixa, wykorzystująca technikę trampoliny, oparta na generatorach. Możliwe jest też to, że trafiłem tam przez notatkę Juliena Couvreur’sa.
Wtedy zdecydowałem się na napisanie czegoś podobnego, bardzo prostego, wykorzystującego generatory, umożliwiającego przetwarzanie kilku funkcji w tym samym czasie, przede wszystkim bez blokowania UI. Zdecydowałem się na generatory, mimo tego iż ograniczało się to tylko do Firefoksa. Nie było to problemem, bo ów silnik w głównej mierze wykorzystywany jest (jedynie) pod tą przeglądarką (cała aplikacja jest rozszerzeniem dla tej przeglądarki).
Oczywiście chodził mi po głowie również pomysł, aby zaimplementować coś podobnego z wykorzystaniem setTimeout
, najlepiej pod takim samym interfejsem, bądź opracowanie wspólnego interfejsu, aby w łatwy sposób można było korzystać z dobrodziejstw biblioteki także pod innymi przeglądarkami. Na pomyśle się skończyło, pewnie z braku czasu, chęci i potrzeby…
Nie będę tutaj opisywał wszystkiego, myślę, że wystarczy ograniczyć się do opisu interfejsu, prostego przykładu tworzącego wątek i ewentualnie, o ciekawszych kawałkach kodu lub zagadnieniach i wyborach projektowych.
Interfejs
Dążenie do jak największej prostoty, w dużej mierze wpłynęło na kształt interfejsu biblioteki. Całość można podzielić na kreatora wątku i menadżera, który zarządza wątkami i steruje ich wykonywaniem za pomocą prostego schedulera. A całość znajduje się w przestrzeni nazw Thread
:
Thread = {
State: {}, // thread states
New: {}, // thread creator
Manager: {}, // thread manager
};
Obiekt Thread.State
możemy potraktować jako enum
zawierający możliwe statusy wątku oraz metodę toString
, konwertująca liczbową wartość statusu na jego nazwę:
Thread.State = {
New: 0, // The thread has been created (but not started).
Running: 1, // The thread has been started (is running).
Terminated: 2, // The thread has been terminated.
Paused: 3, // The thread has been paused.
Killed: 4, // The thread has been killed.
toString: function(state) {}
}
Kreator wątku - Thread.New
- jest funkcją przyjmującą jako parametr referencje do funkcji generatora oraz opcjonalnie callback
, który zostanie uruchomiony, gdy wątek zakończy swoje działanie:
Thread.New = function(func, finish) {}
Funkcja zwraca obiekt wątku, którego interfejs pozwala na sterowanie i kontrole działania utworzonego wątku:
ThreadObj = {
// thread state
state: function() {},
// thread time excution
time: function() {},
// get/set the priority of the thread, between zero and 100
getPriority: function() {},
setPriority: function(priority) {},
// starts the thread execution
run: function() {},
// suspends the thread
pause: function() {},
// resumes a thread suspended by the call to pause()
resume: function() {},
// immediately terminates the target thread
kill: function() {},
}
Thread.Manager
odpowiada za zarzadzanie wątkami, posiada on publiczny interfejs:
Thread.Manager = {
// insert thread data to thread queue
insert: function(thread) {},
// remove thread data from thread queue
remove: function(thread) {},
// has thread exist in thread queue
exist: function(thread) {},
// count of thread in thread queue
count: function() {}
}
Większość metod przyjmuje jako parametr obiekt reprezentujący wewnętrzne dane wątku, zatem jego bezpośrednie używanie jest prawie niepotrzebne - metod tych używa publiczny interfejs wątku, i to z niego powinniśmy korzystać.
Planista
Algorytm szeregowania zaimplementowany w planiście (scheduler) zbliżony jest do algorytmu karuzelowego - Round-robin. W planowaniu rotacyjnym każde zadanie otrzymuje kwant czasu, po spożytkowaniu którego zostaje wywłaszczone i ustawiony na koniec kolejki. W naszej implementacji sami nie potrafimy wywłaszczyć wątku - ograniczenia języka - dokonuje tego sam wątek za pomocą operatora yield
. Po wywłaszczeniu wątku, sterowanie zostaje przekazane do następnego zadania i tak wszystko toczy się w kółko. Przetwarzane w ten sposób zadania co pewien czas są przerywane na odpowiedni okres czasu, w czasie którego kontekst wykonania przekazany jest do silnika JS (przeglądarki). Można rzec, że zostaje wywłaszczony przez główny wątek JS przeglądarki.
W aktualnej implementacji dobrano odpowiednie czasy przetwarzania wątków użytkownika i systemu (silnik JS), aby wykonywujące się zadania użytkownika nie blokowały aktualizacji UI. Oparto to w pewien sposób bazując na płynności wyświetlania obrazów, gdzie aby uzyskać wrażenie płynności wyświetlania, należy zachować minimum 25 klatek/s (TV).
W czasie przetwarzania ciężkich zadań, przewijana przez użytkownika strona, odświeżanie jej zawartości powinno być płynne, bez zauważalnych blokad lub zawierzeń. Bazując na opisanej metodzie i eksperymentach, ustalono poszczególne przedziały czasu: przetwarzanie zadań użytkownika w paczkach po ok. 70ms, rozdzielonych 30ms przerwami dla wątku przeglądarki. Zastosowanie takich wartości spełnia postawione wymagania, blokowanie UI jest niezauważalne.
Należy jednak wziąć pod uwagę fakt, że to wątki same się wywłaszczają, dlatego czas przetwarzania zadania pomiędzy poszczególnymi przerwaniami musi być mały, mniejszy niż ustalony czas obsługi zadań użytkownika. W przeciwnym wypadku blokowanie aktualizacji UI może być zauważalne.
Sam planista, podobnie jak zadania, jest również generatorem. To samo dotyczy nadzorującego go procesu. Można się o tym przekonać analizując źródła. Zastosowano takie zagnieżdżenie generatorów, głównie dlatego, aby w czasie spoczynku, gdy żadne zadanie nie znajduje się w kolejce aktywnych wątków, nie były zajmowane jakiekolwiek zasoby - nic się nie działo i nie kręciło.
Obecna implementacja planisty nie bierze pod uwagę priorytetów wątków, jakie można nadać poszczególnym zadaniom.
Użycie
Wykorzystanie biblioteki jest bardzo proste. Dołączono do biblioteki prosty przykład prezentujący użycie i wykorzystanie możliwości przez nią oferowanych.
Jako prosty przykład możemy rozpatrzy dwa wątki/zadania prostej funkcji licznika, zapisanej następująco:
function count(name, n, k) {
for (var i=0; i<n; i++) {
console.log(name + ': ' + i);
// sleeep
for (var j=0; j<1000000; j++) {}
if (!(i%k)) yield;
}
}
Tworzymy domknięcie funkcji licznika dla każdego wątku z inną nazwą oraz przekazujemy callback, który poinformuje nas o zakończeniu pracy wątku, i uruchamiamy…
Thread.New(
count('foo', 100, 2),
function(thread) { console.log('foo complete: ' + thread.time() + 'ms'); }
).run();
Thread.New(
count('bar', 100, 2),
function(thread) { console.log('bar complete: ' + thread.time() + 'ms'); }
).run();
Szerszy komentarz uważam za zbędny i nie potrzebny…
Zakończenie
Biblioteka Thread.js dostępna jest na projects.malcom.pl. Można znaleźć tam namiary na źródła leżące na githubie, z załączonym, prostym przykładem wykorzystania. Rozpowszechniana jest na zasadach licencji MIT.
Mam nadzieje, że komuś się przyda, albo stanie inspiracją do własnych testów i eksperymentów oraz implementacji, o których chętnie poczytam ;)
Komentarze (0)