Asembler, jak wszystkie inne strukturalne języki programowania pozwala pisać programy, w których ścieżka wykonywanych instrukcji jest tylko jedna. Mogą być rozwidlenia i pętle, ale zawsze wykonuje się tylko jedna rzecz na raz.
Wątki pozwalają na uruchomienie wielu niezależnych ścieżek, które będą wykonywane równolegle. Daje to duże możliwości programom, które wykonują kilka czynności na raz (na przykład czytanie z jednego pliku i zapisywanie przetworzonych danych do drugiego). Zysk jest też w programach sieciowych, a zwłaszcza serwerach. Po dodaniu obsługi wątków możliwe jest połączenie więcej niż jednego klienta w danej chwili. Ale przejdźmy wreszcie do szczegółów.
Najpierw omówię trzy funkcje z biblioteki języka C (ściśle mówiąc, z biblioteki pthreads), które pozwolą nam zarządzać wątkami.
Funkcja ta przyjmuje 4 argumenty. Od lewej (ostatni wkładany na stos) są to:
Funkcja ta kończy bieżący wątek. Wartość podana jako jedyny jej argument (adres danych) może być wykorzystana przez wątki podłączone (pthread_join) do tego wątku. Po zakończeniu wszystkich wątków, program kończy działanie z kodem 0.
Oczywiście, system operacyjny sam też przydziela czas procesora poszczególnym wątkom, ale wywołując tę funkcję możemy powiedzieć, by skrócił czas przeznaczony dla tego wątku i dał go innym. Przydaje się, gdy bieżący wątek chwilowo skończył pracę (na przykład zabrakło danych itp.). Funkcja nie przyjmuje żadnych argumentów.
Poniżej przedstawiam króciutki program, który pokaże, jak to wszystko działa. Program ma jeden raz wyświetlić napis pierwszy w funkcji głównej i 5 razy napis drugi w funkcji wątku.
; Przykładowy program wielowątkowy w asemblerze ; ; Autor: Bogdan D., bogdandr (at) op.pl ; ; kompilacja: ; nasm -O999 -f elf -o watki.o watki.asm ; gcc -o watki watki.o -lpthread section .text global main ; deklaracje funkcji zewnętrznych extern pthread_create extern pthread_exit main: mov eax, 4 mov ebx, 1 mov ecx, napis1 mov edx, napis1_dl int 80h ; wyświetlamy napis pierwszy push dword 0 ; dodatkowe dane push dword watek ; adres funkcji do uruchomienia push dword 0 ; atrybuty push dword id_watku ; gdzie zapisać ID call pthread_create ; utworzenie nowego wątku ; Nie należy wychodzić z programu funkcją sys_exit (EAX=1), gdyż ; zakończyłoby to wszystkie wątki programu. Zamiast tego, zamykamy tylko ; wątek główny. push dword 0 call pthread_exit ; zakończenie bieżącego wątku watek: mov dword [t1+timespec.tv_nsec], 0 mov dword [t1+timespec.tv_sec], 5 ; 5 sekund mov esi, 5 ; napis drugi wyświetlimy 5 razy .petla: mov eax, 162 ; sys_nanosleep mov ebx, t1 ; adres struktury mówiącej, ; ile chcemy czekać mov ecx, 0 int 80h ; robimy przerwę... mov eax, 4 mov ebx, 1 mov ecx, napis2 mov edx, napis2_dl int 80h ; wyświetl napis drugi dec esi jnz .petla ; wykonuj pętlę, jeśli ESI != 0 push dword 0 call pthread_exit ; zakończenie bieżącego wątku section .data napis1 db "Funkcja glowna.", 10 napis1_dl equ $ - napis1 napis2 db "Watek.", 10 napis2_dl equ $ - napis2 struc timespec .tv_sec: resd 1 .tv_nsec: resd 1 endstruc t1 istruc timespec id_watku dd 0 ; zmienna, która otrzyma ID nowego wątku
Ale wątki w programie to nie tylko same zyski. Największym problemem w programach wielowątkowych jest synchronizacja wątków.
Po co synchronizować? Po to, żeby program nie sprawiał problemów, gdy dwa lub więcej wątków odczytuje i zapisuje tę samą zmienną globalną (na przykład bufor danych).
Co zrobić, by na przykład wątek czytający przetwarzał dane dopiero wtedy, gdy inny wątek dostarczy te dane? Możliwości jest kilka:
Na przykład ustalmy, że jeśli flaga jest równa zero, to bufor może być dowolnie używany (do zapisu i odczytu). Jeśli flaga jest równa na przykład jeden, to nie wolno wykonywać operacji na buforze (bo inny wątek już to robi) - należy poczekać, aż flaga będzie równa zero.
Zaletą tego rozwiązania jest prostota jego utworzenia. Popatrzcie:
flaga db 0 ... watek: ... sprawdz_flage: cmp byte [flaga], 1 je sprawdz_flage mov byte [flaga], 1 ... ; tutaj nasze operacje mov byte [flaga], 0
Jak widać, pisanie programów wielowątkowych nie jest takie trudne, warto więc się tego nauczyć. Tym bardziej, że zyski są większe (napisanie po jednej funkcji na każde oddzielne zadanie), niż wysiłek (synchronizacja).
Oczywiście, aby pisać programy wielowątkowe, nie musicie korzystać z żadnej biblioteki. Odpowiednie mechanizmy posiada sam interfejs systemu - przerwanie int 80h.
Skorzystam tutaj z funkcji sys_fork (numer 2). Jej jedynym argumentem jest adres struktury zawierającej wartości rejestrów dla nowego procesu, ale ten argument jest opcjonalny i może być zerem. Funkcja fork zwraca wartość mniejszą od zera, gdy wystąpił błąd, zwraca zero w procesie potomnym, zaś wartość większą od zera (PID nowego procesu) - w procesie rodzica. Proces potomny zaczyna działanie tuż po wywołaniu funkcji fork, czyli rodzic po wykonaniu funkcji fork i potomek zaczynają wykonywać dokładnie te same instrukcje. Procesy te można skierować na różne ścieżki, sprawdzając wartość zwróconą przez fork w EAX.
Oto krótki przykład w składni FASMa:
format ELF executable entry _start segment executable _start: mov eax, 2 ; funkcja fork xor ebx, ebx int 80h ; wywołanie cmp eax, 0 jl .koniec ; EAX < 0 oznacza błąd ; poniższe instrukcje wykona zarówno rodzic, jak i potomek: cmp eax, 0 jg .rodzic ; EAX > 0 oznacza, że jesteśmy w ; procesie rodzica ; tutaj ani EAX < 0, ani EAX > 0, więc EAX=0, czyli ; jesteśmy w procesie potomka ; kod poniżej (wyświetlenie i czekanie) wykona tylko potomek mov dword [t1.tv_nsec], 0 mov dword [t1.tv_sec], 5 ; tyle sekund przerwy będziemy robić ; między wyświetlaniem napisów .petla: mov eax, 4 ; funkcja zapisywania do pliku mov ebx, 1 ; standardowe wyjście mov ecx, napis2 ; co wypisać mov edx, napis2_dl ; długość napisu int 80h mov eax, 162 ; funkcja sys_nanosleep mov ebx, t1 ; tyle czekać mov ecx, 0 ;ewentualny adres drugiej struktury timespec int 80h ; robimy przerwę... jmp .petla ; i od nowa.... ; kod poniżej (wyświetlenie i wyjście) wykona tylko rodzic .rodzic: mov eax, 4 ; funkcja zapisywania do pliku mov ebx, 1 ; standardowe wyjście mov ecx, napis1 ; co wypisać mov edx, napis1_dl ; długość napisu int 80h .koniec: mov eax, 1 ; funkcja wyjścia z programu xor ebx, ebx int 80h segment readable writeable napis1 db "Rodzic", 10 napis1_dl = $ - napis1 napis2 db "Potomek", 10 napis2_dl = $ - napis1 struc timespec ; definicja struktury timespec ; (tylko jako typ danych) { .tv_sec: rd 1 .tv_nsec: rd 1 } t1 timespec ; tworzymy zmienną t1 jako całą strukturę