Pisanie programów wielowątkowych pod Linuksem

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.

  1. pthread_create - tworzenie nowego wątku.

    Funkcja ta przyjmuje 4 argumenty. Od lewej (ostatni wkładany na stos) są to:


  2. pthread_exit - zakończenie bieżącego wątku

    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.

  3. pthread_yield - oddanie czasu procesora innym wątkom lub procesom

    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.


(przeskocz program)
; 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:

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


Wielowątkowość z przerwaniem 80h

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ę


Spis treści off-line (Alt+1)
Spis treści on-line (Alt+2)
Ułatwienia dla niepełnosprawnych (Alt+0)