Thread.js

8 stycznia 2013

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 karuzelowegoRound-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 ;)

Podobne notatki:

Może zainteresują Cię również następujące, pododbne notatki:

Nikt jeszcze nie skomentował tego wpisu.
Możesz być pierwszy.

Dodaj swój komentarz

Możesz użyć tych tagów XHTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Jeśli chcesz wstawić kilku linijkowy fragment kodu, użyj tagów <pre lang="x"></pre> (gdzie x język kodu np. cpp, perl, html). W ten sposób kod zostanie odpowiednio sformatowany i pokolorowany przez system.

Uwaga!

Na tym blogu działa system cache oraz filtr antyspamowy. Twój komentarz może być widoczny na stronie z pewnym opóźnieniem. Proszę o cierpliwość. Jeśli utraciłeś już wszystkie jej zasoby poinformuj mnie o tym, być może system uznał Cię za spamera ;)