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:
- std.stdio - standardowe wejście i wyjście. Dzięki temu będziemy mogli czytać i pisać na konsoli.
- std.thread - biblioteka obsługi wątków udostępniająca klasę Thread.
- std.string - parę funkcji do manipulacji ciągami tekstu.
- 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 :).
- 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:
- new - tworzy nowy wątek.
- list - wyświetla status wszystkich aktualnych wątków.
- remove x - usuwa wątek o numerze X, pod warunkiem że istnieje.
- 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 :).






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