Dziś jest sobota, 10 stycznia 2009 roku (z kalendarza...)

Język D: Wielowątkowość

18 czerwca pisałem na Zyxist.com o języku programowania D, który śmiało może konkurować o miano oficjalnego następcy/spadkobiercy języków C oraz C++. Wreszcie znalazłem czas, aby napisać w nim pierwszy niehelloworldowy program, którego omówieniem zajmiemy się w tym wpisie. Na tapetę poszła wielowątkowość uzyskana za pomocą biblioteki standardowej zwącej się Phobos. Program dodatkowo pokazuje, w jaki sposób unikać zabawy ze wskaźnikami oraz jak manipulować ciągami tekstu.

Zasada działania programu jest debilnie prosta. Po uruchomieniu dostajemy linię komend, w której odpowiednimi poleceniami możemy tworzyć, kasować oraz sprawdzać status wątków. Jest też oczywiście polecenie wyjścia. Jeśli chciałbyś go potestować lub samodzielnie spróbować swych sił, musisz wpierw zaopatrzyć się w jakiś kompilator: oficjalny DMD lub port do GCC, GDC. Na dzień dzisiejszy różnice między nimi są minimalne i powinno ich być stopniowo coraz mniej. Instrukcje instalacji można znaleźć np. na polskim Wikibooks.

Zacznijmy zatem pisanie. Na początek ładujemy kilka modułów Phobosa:

import std.stdio;
import std.thread;
import std.string;
import std.conv;
import std.c.time;

Oto krótkie omówienie poszczególnych modułów:

  1. std.stdio - standardowe wejście i wyjście. Dzięki temu będziemy mogli czytać i pisać na konsoli.
  2. std.thread - biblioteka obsługi wątków udostępniająca klasę Thread.
  3. std.string - parę funkcji do manipulacji ciągami tekstu.
  4. std.conv - biblioteka konwersji typów. Umożliwia zamianę fragmentu tekstu na liczbę żadanego typu. Od podobnych funkcji w C odróżnia ją to, że należy jej podawać ciąg tekstowy, który poza cyframi nie może zawierać nic innego, np. spacji, tabulacji itd. Za to nie ma w nich żadnych pogrzabnych i niepotrzebnych parametrów, których przeznaczenie i sens istnienia rozumieliby tylko ich autorzy :).
  5. std.c.time - D jest na poziomie binarnym w pełni zgodny z językiem C, stąd można wykorzystywać jego biblioteki bez potrzeby przepisywania ich oraz rekompilacji. std.c.time jest interfejsem odpowiadającym plikowi time.h z C.

Wątek w języku D jest w rzeczywistości obiektem klasy dziedziczącej po Thread i nadpisującej główną metodę run. Tak więc manipulacja wątkami sprowadza się do tworzenia oraz usuwania odpowiednich obiektów. Napiszemy teraz klasę programThread, która implementuje metodę run() tak, aby co sekundę uaktualniała wewnętrzny licznik o 1. Poprzez sprawdzenie stanu wątku będziemy zatem rozumieć odczytanie stanu licznika.

class programThread: public Thread
{
	private
	{
		long txx = 0;
		long lastsec = 0;
	}
	
	public int run()
	{		
		while(1)
		{
			time_t r = time(null);
			if(lastsec != cast(long)r)
			{
				lastsec = cast(long)r;
				txx++;
			}		
		}
		
		return 0;	
	} // end run();
	
	public long getCounter()
	{
		return txx;
	} // end getCounter();
} // end programThread; 

W stosunku do C++ jest tu kilka różnic. Po pierwsze, deklaracji klasy nie trzeba kończyć średnikiem. Ponadto ustandaryzowany został sposób określania widoczności metod oraz pól. Słowa takie, jak public, protected oraz private są tzw. atrybutami, do których stosują się jednolite reguły składni. Możemy ich używać podobnie, jak w PHP:

private long pole;
private long innePole;

Lecz dozwolone jest także tworzenie większych bloków zamkniętych w nawiasach klamrowych:

	private
	{
		long txx = 0;
		long lastsec = 0;
	}

Atrybutów jest o wiele więcej, lecz wszystkie używają jednolitych reguł składniowych. Wróćmy jednak do naszej klasy. Metoda run() zawiera główny kod wątku. Jeśli chcemy, aby wykonywał się on w tle, musimy umieścić w nim nieskończoną pętlę, która w każdej iteracji sprawdza aktualny czas i porównuje go z pamiętanym stanem. W razie różnicy zwiększa licznik. Dodatkowo zdefiniowaliśmy metodę służącą do odczytu licznika.

Nasz program będzie udostępniać następne polecenia:

  1. new - tworzy nowy wątek.
  2. list - wyświetla status wszystkich aktualnych wątków.
  3. remove x - usuwa wątek o numerze X, pod warunkiem że istnieje.
  4. quit - kończy program.

Zauważmy, że jedna z komend pobiera liczbowy parametr, który musimy wyciąć z tekstu wprowadzonego przez użytkownika tak, aby nie spodować błędu funkcji konwersji, jako że nie można jej podawać wraz z liczbą np. białych znaków. Dlatego napiszemy funkcję extractNumber(), która wytnie fragment ciągu zawierający nieprzerwany ciąg cyfr i nic więcej.

char [] extractNumber(char [] str)
{
	uint start = -1, stop = -1;
	uint i;
	for(i = 0; i < str.length; i++)
	{
		if(str[i] >= 48 && str[i] <= 57)
		{
			if(start == -1)
			{
				start = i;
			}
		}
		else if(start != -1)
		{
			stop = i;
			break;
		}	
	}
	return str[start..stop];
} // end extractNumber();

Każdy ciąg w języku D jest uznawany za dynamiczną tablicę, czyli char []. Podobnie jak niektóre inne typy danych, tablice posiadają atrybuty dostarczające nam niekiedy użytecznych danych. Przykładowo, tutaj zadeklarowaliśmy, że funkcja wymaga parametru str będącego dowolnym ciągiem tekstu. Aby sprawdzić jego długość, wystarczy skorzystać z atrybutu length: str.length. Gdy jesteśmy już w pętli, do poszczególnych znaków odwołujemy się identycznie, jak w C oraz C++. Jednak jest pewna różnica między tymi językami. Mianowicie w D ciągi tekstu nie są kończone znakiem \0. Jeżeli korzystamy z jakichś funkcji języka C, musimy pamiętać, aby dokleić go samodzielnie.

Nasza pętla określa pozycję początkowej oraz końcowej cyfry w ciągu (pomiędzy tymi pozycjami oczywiście są wyłącznie inne cyfry). Możemy teraz bardzo łatwo wyciąć ten fragment, korzystając bezpośrednio ze struktur języka: return str[start..stop]. Analogiczna składnia działa również dla zwykłych tablic. Przechodzimy teraz do funkcji main(). Pierwsze, co robimy, to deklarujemy jakąś tablicę na przechowywanie aktualnych wątków:

void main()
{
	programThread [int] threads;
	uint threadNum = 0;

W nawiasach klamrowych podaliśmy typ int, dzięki czemu otrzymujemy tablicę haszującą z kluczami liczbowymi. Dzięki temu jesteśmy w stanie swobodnie manipulować zawartością, dodając i usuwając niepotrzebne elementy bez szkody dla całości. Ponadto możemy sprawdzać, czy element o podanym kluczu istnieje w tablicy. Gdybyśmy zamiast int podali char [], otrzymalibyśmy tablicę asocjacyjną z indeksami tekstowymi: tablica["pole"] = wartosc. Z kolei niepodanie typu indeksu spowoduje, że dostaniemy coś zbliżonego kształtem do tablic znanych z C oraz C++, gdzie musielibyśmy samodzielnie pisać mechanizmy sprawdzania, czy dane pole jest wolne, czy nie itd.

Teraz kolej na główną pętlę i pobranie komendy od użytkownika:

	while(1)
	{
		writefln("Enter command: ");
		char cmd[] = readln();

Podstawowe funkcje wejścia/wyjścia są zbliżone funkcjonalnością bardziej do tego, co oferował Pascal (mi osobiście takie coś w pełni odpowiada). Za pomocą funkcji std.string.find() możemy sprawdzić, czy w podanym ciągu znajduje się stosowna komenda:

		if(std.string.find(cmd, "quit") == 0)
		{
			break;
		}
		if(std.string.find(cmd, "new") == 0)
		{
			threads[threadNum] = new programThread;
			threads[threadNum].start();
			writefln("New thread: ", threadNum);
			threadNum++;
		}

Jak mówiliśmy, "quit" kończy program, a "new" tworzy nowy wątek. Odbywa się to tak, że do następnego w kolejności indeksu wskazywanego przez zmienną threadNum wprowadzamy nowy obiekt i uruchamiamy go metodą start(). Następnie wyświetlamy komunikat o tym, co się stało, jaki numer przydzieliliśmy wątkowi i zwiększamy licznik wątków o jeden.

W kodzie kolejnej komendy, "remove", skorzystamy z utworzonej wcześniej funkcji extractNumber() do wycięcia numeru wątku:

		if(std.string.find(cmd, "remove") == 0)
		{
			uint id = std.conv.toUshort(extractNumber(cmd));
			
			if((id in threads) != null)
			{
				threads.remove(id);
			}
 
			writefln("Removing thread:", id);
		}

W tej części zaprezentowany został ciekawy sposób sprawdzenia, czy w tablicy threads istnieje element o podanym indeksie: (id in threads) != null. Gdyby to dodatkowo podstawić do zmiennej, otrzymalibyśmy wskaźnik do znalezionego elementu. Wskazany element możemy usunąć z tablicy "metodą" remove().

Pozostała nam jeszcze komenda "list". Jej obsługa składa się z trzech pętli. Pierwsza zatrzymuje wszystkie wątki, druga wyświetla status każdego z nich, trzecia z powrotem je uruchamia. Można oczywiście sprawdzać każdy po kolei, lecz wtedy w mojej opinii byłoby to mniej "miarodajne" (dalsze wątki zdążyłyby nabić trochę do czasu ich zastopowania i sprawdzenia).

		if(std.string.find(cmd, "list") == 0)
		{
			for(uint i = 0; i < threadNum; i++)
			{
				if((i in threads) != null)
				{
					threads[i].pause();
				}
			}
			for(uint i = 0; i < threadNum; i++)
			{
				if((i in threads) != null)
				{
					writefln("Thread #", i, ": ",threads[i].getCounter());
				}
			}
			for(uint i = 0; i < threadNum; i++)
			{
				if((i in threads) != null)
				{
					threads[i].resume();
				}
			}
		}

Pozostaje nam już na zakończenie usunąć wszystkie istniejące wątki oraz zamknąć program:

	for(uint i = 0; i < threadNum; i++)
	{
		if((i in threads) != null)
		{
			threads.remove(i);
		}
	}
	writefln("Thank you for using our threaded server application.");
} // end main();

Napisałem też drugą wersję tego programu, która robi dokładnie to samo, co pierwsza, ale jest inaczej napisana, jeśli chodzi o mechanizm przetwarzania poleceń. Zamiast wyszukiwać takowe funkcją find(), prosty parser dzieli nam ciąg na części, które później możemy badać zwykłą instrukcją switch (w przeciwieństwie do C/C++, ta działa też z tekstem). Początek programu pozostaje taki sam. Wywalamy funkcję extractNumber() i zamiast niej wstawiamy taką oto klasę stmt:

class stmt
{
	char [] instruction;
	uint arg;
	
	this(char [] cmd)
	{
		uint start = -1, stop = -1;
		uint status = 0;
		uint i;
		for(i = 0; i < cmd.length; i++)
		{
			switch(status)
			{
				case 0:
					if(cmd[i] < 97 || cmd[i] > 122)
					{
						if(i == 0)
						{
							reset();
							break;
						}
						status ++;
						instruction = cmd[0 .. i];
					}
					break;
				case 1:
					if(cmd[i] >= 48 && cmd[i] <= 57)
					{
						if(start == -1)
						{
							start = i;
						}
					}
					else if(start != -1)
					{
						stop = i;
						break;
					}
					arg = std.conv.toUshort(cmd[start..stop]);
					break;
			}		
		}
	} // end this();
	
	void reset()
	{
		instruction = "";
		arg = 0;
	} // end reset();
} // end stmt; 

Tutaj przy okazji można zobaczyć, że konstruktory i destruktory także tworzy się inaczej, niż w C++. Zamiast nazwy klasy, stosujemy nazwę this. Po wprowadzeniu wypocin użytkownika do konstruktora w obiekcie tej klasy zostaną nam dwie wartości: instruction z nazwą polecenia oraz arg z opcjonalnym parametrem dodatkowo przekonwertowanym już na liczbę. Musimy teraz na nowo napisać pętlę while w funkcji main():

	general:while(1)
	{
		writefln("Enter command: ");
		stmt line = new stmt(readln());
		switch(line.instruction)
		{
			case "quit":
				break general;
			case "new":
				threads[threadNum] = new programThread;
				threads[threadNum].start();
				writefln("New thread: ", threadNum);
				threadNum++;
				break;
			case "remove":
				if((line.arg in threads) != null)
				{
					threads.remove(line.arg);
				}
	
				writefln("Removing thread:", line.arg);
				break;
			case "list":
				for(uint i = 0; i < threadNum; i++)
				{
					if((i in threads) != null)
					{
						threads[i].pause();
					}
				}
				for(uint i = 0; i < threadNum; i++)
				{
					if((i in threads) != null)
					{
						writefln("Thread #", i, ": ",threads[i].getCounter());
					}
				}
				for(uint i = 0; i < threadNum; i++)
				{
					if((i in threads) != null)
					{
						threads[i].resume();
					}
				}
				break;
			default:
				writefln("Unknown command");
		}
	}

C++ umożliwiał za pomocą break przerywanie np. pętli leżących dwa poziomy wyżej. Należało zwyczajnie podać numer, ile poziomów w górę należy wyskoczyć. Język D zamiast tego stosuje etykiety. Tutaj nadaliśmy etykietę pętli while: general:while(1). Możemy teraz ją w dowolnym momencie przerwać, wpisując break general;

Chciałbym zwrócić uwagę na pewną ciekawą rzecz. Mianowicie do napisania tego programu w ogóle nie musiałem używać wskaźników, referencji itd. Jest to kolejna cecha tego języka, którego założeniem jest umiejętność samodzielnego dobrania stosownego środka rozwiązania danego problemu w zależności od potrzeb oraz wykorzystania. Wskaźniki jako takie oczywiście są dostępne, ale zarządzanie nimi to moim zdaniem jedna z bardziej wkurzających rzeczy, gdy trzeba je ręcznie aplikować do każdej głupoty.

Nad kodem tego programiku spędziłem nieco czasu, ale głównie z powodu braku wystarczającej ilości materiałów o języku. Na początku zawsze wszystko idzie wolniej, ponieważ nie wiemy jeszcze, na co możemy sobie pozwolić i w jaki sposób korzystać z proponowanego nam arsenału środków. Za drugim razem już kiwa się głową z uśmiechem: "jakie to banalne". Ogólnie po pierwszym poważniejszym starciu z językiem D jestem zadowolony, gdyż rzeczywiście programuje się w nim przyjemniej, niż w C/C++, w dodatku bez konieczności rezygnacji z niskopoziomowych możliwości, gdy jest taka potrzeba. Normalnie jak Open Power Template :).

Powrót

Komentarze

Napisał radzio w sobotę, 7 lipca 2007 o 10:56

Dawno nie bawiłem się w języku D, wygląda na to, że już pora ;-)

Napisał Zyx w niedzielę, 8 lipca 2007 o 14:56

Program się rozwija, właśnie dodałem do niego obsługę socketów i teraz można tworzyć wątki za pomocą telnetu :]

Napisał NuLL w niedzielę, 8 lipca 2007 o 20:33

Jezyk D po Pythonie jest w kolejce : ) Zyx - daloby sie zrobic klikalne tytuły wpisow ? : )

Pozdro

Napisał Zyx w poniedziałek, 9 lipca 2007 o 14:29

Naturalnie - mówisz, masz :).

Napisał NuLL w poniedziałek, 9 lipca 2007 o 18:32

Dzieki - klikanie w tytul wydaje sie byc bardziej naturalne.

Napisał blid w wtorek, 10 lipca 2007 o 11:49

Niedawno zacząłem poznawać Pythona a już widzę kilka podobieństw do D, ciekawe, czy w kolejnych twoich notkach znajdę kolejne:) Podobieństwo do Pythona można zaliczyć tylko na plus.
Poprawione pewne nieintuicyjne zachowania c++, możliwość importu bibliotek z C to dla migrujących - kolejne zalety.
Żeby tylko był bardziej popularny..

Napisał Coldpeer w środę, 11 lipca 2007 o 15:35

blid: popularny to D będzie, jak się bardziej rozwinie ;) O to się nie martw, w końcu dopiero pół roku temu wyszła pierwsza stabilna wersja.

Napisał Zyx w środę, 11 lipca 2007 o 17:14

W necie można już znaleźć kilka opinii różnych programistów. Trafiłem na stronę, na której autor opisywał zalety C++, a na końcu dodał: "Z niecierpliwością czekam na spopularyzowanie się jeszcze lepszego języka od C++ a mianowicie języka D". Z kolei na liście dyskusyjnej inny programista C++ krytykował D za "przeficzerowanie" (za TAKIE spolszczenia słów powinni karać wysokimi grzywnami) oraz bronił składni swego ulubionego języka.

Napisał Witek Baryluk w niedzielę, 1 czerwca 2008 o 14:02

Kilka uwag:

1. Zamiast time() z std.c.time polecam getUTCtime() z std.date

2. W pętli for można bezpośrednio definiować zmienne:
for (int i = 0; i < cmd.length; i++)
zamiast
uint i;
for (i = 0; i < cmd.length; i++)

3. Do indeksowania pętli zalecą się używać typu size_t;

4. A jeszcze lepiej, po prostu:
foreach (i, c; cmd) {
// c zamiast cmdi
}
// zmienne i oraz c same się otypią

5. funkcję extractNumber łatwo można zaimplementowąć używając std.regexp. Ale dobrze że tu jest bo pokazuje jak się uzywa tablice :)

6. stmt też można zrobić przy pomocy std.regexp
7. lub przy pomocy std.string split() ! bardzo fajna rzecz

8. warto wspomnieć że wycinki (slice) jak cmdstart..end powoduje współdzielenie danych. tak więc trzeba być ostrożnym przy zapisywaniu do tablic. oczywiście póki używamy garbage collectora, a nie ręcznie zwalniamy pamięć nie musimy się bardzo matrwić. może to być ważne np. w przypadku kiedy argument funkcji extractNumber pochodzi z funkcji read której przekazuje się dodatkowy bufor (w celach eliminacji dodatkowych allokacji przy każdym jej wywołaniu) - jesli za drugim razem przekazaemy ten sam bufor (ktory gdzies tam uzylismy i zrobilismy do niego wycinek (slice), to teraz moze sie on zmienic!). dlatego warto wiedziec co sie robi, albo kopiowac wycinki: return cmdstart..end.dup

9. Zamiast stosować 48 czy 57, wystarczy użyć '0' '9', czytelniejsze i wężej się otypia.

10. w głównej pętli można zastosować kilka ciekawych trików (szczególnie jeśli rodzajów poleceń było by dużo - aktualnie switch sprawdza stringi po kolei!):

alias (void delegate()) deg;
degchar[] pol;

pol"quit" = { break general; }
pol"new" = {
threadsthreadNum = new programThread;
threadsthreadNum.start();
writefln("New thread: ", threadNum);
threadNum++; // BTW. to powinno być synchronized { } :)
}


...

general:
...
stmt line = new stmt(readln());

auto p = (line.instruction in pol);
if (p) {
p();
}
goto general;


Powodzenie w poznawaniu D!


Autor podręcznika D na Wikibooks (w stanie spoczynku aktualnie :D )

Strona 1 z 1 :: 1

Skomentuj

NickInformacja
E-mailTylko do użytku wewnętrznego.
WWWNie zapomnij o http://
LayoutNapisz tu, czy widzisz dzienny czy nocny layout.
WpisFormatowanie wiki
Internauto, pamiętaj! Wolność to nie samowola - dbaj o kulturę wypowiedzi oraz dyskusji w sieci.

Na Zyxist.com panuje swoboda wyrażania opinii oraz krytyki pod dowolnym adresem. Jedyny warunek: musi być ona kulturalna i rzeczowa. Na chamstwo, prostactwo lub jawne obrażanie kogokolwiek nie ma tu miejsca i takie komentarze są bardzo szybko usuwane. Jeśli zamierzasz polemizować z treścią wpisu, wpierw uważnie ją przeczytaj.

© Tomasz "Zyx" Jędrzejewski 2005 - 2009 | Wykonanych zapytań: 2 | Serwer wirtualny zapewnia