2007.02_Programowanie równoległe z Qt_[Programowanie].pdf

(279 KB) Pobierz
439030760 UNPDF
dla programistów
Programowanie równoległe z Qt
Programowanie
Piotr Pszczółkowski
Bardzo często, w programach wymagających dużych szybkości przetwarzania, musimy posiłkować
się mechanizmami przetwarzania równoległego. Można to porównać do człowieka, który robi wiele
rzeczy na raz (np. czyta książkę, jednocześnie słucha muzyki i głaszcze swojego ulubionego kota).
Oczywiście mógłby to wszystko zrobić osobno, skupiając się tylko na jednej, właśnie wykonywanej,
czynności. Ale zajęłoby to wielokrotnie więcej czasu. Dokładnie tak samo jest z programami
komputerowymi.
nymi przez komputer są procesy
(ang. process) i wątki (ang. thre-
ad). Wątek to ciąg poleceń maszy-
nowych wykonywanych przez procesor kom-
putera, mających na celu wykonanie określone-
go zadania. Relacje zachodzące pomiędzy pro-
cesem i wątkami można opisać używając analo-
gii. Można powiedzieć, że proces to dom (zwy-
kły mieszkalny dom) a wątki to mieszkańcy te-
goż domu. Dom to taki pojemnik o określonych
atrybutach (adres, liczba pokoi, powierzchnia
mieszkalna itd.), czyli takie wydzielone środo-
wisko dla jego mieszkańców. Dom (jak i pro-
ces) tak naprawdę nic nie robi, jest to obiekt
pasywny. Aktywni są natomiast jego miesz-
kańcy, oglądają TV, odrabiają lekcje, piszą ar-
tykuły dla gazet itd. Tak też jest i z wątkami,
to one wykonują kod maszynowy, wykonu-
ją nakreślone przez programistę zadania. Pro-
ces jest środowiskiem, w którym pracują wąt-
ki. Jedna uruchomiona przez nas aplikacja to je-
den proces. Ta sama aplikacja uruchomiona po-
nownie, lub sklonowana poprzez użycie pole-
cenia fork, to kolejny proces. Każdy proces ma
co najmniej jeden wątek. Ale (i to jest to, co nas
interesuje) jeden proces może uruchomić wie-
le wątków, z których każdy będzie zajmować
się swoją własną pracą. Wyniki tych prac oczy-
wiście muszą(/mogą) być udostępnione pozo-
stałym elementom składowym procesu. Możli-
wa jest także bardzo ścisła kooperacja pomię-
dzy wątkami.
Zasada ogólna:
W dalszej części artykułu interesują nas już
tylko wątki. Z tej przyczyny, że uruchamia-
nie procesu poprzez exec, lub sklonowanie
poprzez fork, jest dla systemu operacją skom-
plikowana, przez co i czasochłonną.
wego wkładu pracy programisty tworzącego
aplikację. Wszystkie wątki są w tej samej prze-
strzeni adresowej (przestrzeni adresowej pro-
cesu, który je uruchomił). Mają dostęp do
wszystkich zasobów (np. zmiennych) procesu.
Jedyne co ma na własność każdy wątek, to jest
zawartość stosu (czyli np. zmienne lokalne w
funkcji wątku) i zawartość rejestrów proceso-
ra. Problemem jest to, że mogą chcieć używać
wspólnych zasobów JEDNOCZEŚNIE. A na
to niestety, nie możemy pozwolić. Pozwolenie
na jednoczesny dostęp, to najprostsza droga do
katastrofy. Podstawowa zasada: kilka wątków
nie może JEDNOCZEŚNIE mieć dostępu do te-
go samego zasobu.
Wątki – czy to się opłaca?
W większości przypadków - tak. Można by po-
wiedzieć „jak najbardziej”. W przypadku kom-
puterów wieloprocesorowych lub kompute-
rów z procesorem HT ( Hyper Threading ), czy
wielordzeniowych ( MultiCore ) odpowiedź jest
oczywiście pozytywna. Natomiast w przypad-
ku innego, „zwykłego” procesora, może być
różnie. Jeżeli istnieje tylko jeden potok przetwa-
rzania w procesorze, to co nam po tym że uży-
wamy wątków? Przecież czas pracy procesora
zużyty przez jeden z wątków, zostanie odebra-
ny innemu. Sumarycznie program powinien
trwać tyle samo. Owszem. Byłoby tak gdyby
każdy z wątków wykonywał swoją pracę nie-
przerwanie. Ale w normalnym programie użyt-
kowym tak nie jest. Wątek (-ki) odpowiedzial-
ny za komunikację z użytkownikiem, z punktu
widzenia procesora, jest prawie ciągle w sta-
nie oczekiwania na reakcje użytkownika. W tym
czasie inny watek (-ki) przetwarzający może
zrobić coś naprawdę pożytecznego. I to w spo-
sób niezauważalny przez użytkownika (tzn.
użytkownik albo by nie zaobserwował ob-
ciążenia sytemu, lub byłoby ono nieznaczne).
Podsumowując: w normalnej aplikacji użytko-
wej użycie wątków jest prawie zawsze sen-
sowne.
Mutex – sposób
na współdzielenie zasobów
Aby uniknąć jednoczesnego dostępu do tego
samego zasobu przez kilka wątków, wymyślo-
no coś co się nazywa mutex. Nazwa pochodzi
z połączenia części dwóch słów: mut jak “mu-
tual” (pol. wzajemny, wspólny, obopólny) i ex
jak “exclusion” (pol. wyłączenie, wykluczenie,
usunięcie). Czyli mówiąc po polsku, muteks
WYKLUCZA WSPÓLNY dostęp do chronio-
nego przez niego zasobu (np. zespołu zmien-
nych). Jest to jakby wartownik który pilnuje
aby dany zasób obsługiwał w danej chwili tyl-
ko i wyłącznie jeden wątek.
QThread i QMutex
dla programujących w Qt
Biblioteka Qt zawiera klasy QThread i QMu-
tex pozwalające programiście w sposób nieza-
leżny od platformy, na proste i szybkie tworze-
nie aplikacji z użyciem wątków. Innymi bar-
dzo pożytecznymi klasami są QWaitCondition
i QMutexLocker.
• Każdy proces to co najmniej jeden wą-
tek.
• Wątek należy tylko i wyłącznie do jedne-
go procesu (tego który go uruchomił).
Problemy też są
Oprócz oczywistych zalet stosowania wątków
(przyspieszenie pracy programu) istnieją też
wady. Taką wadą jest konieczność dodatko-
66
luty 2007
równoległe z Qt
P odstawowymi pojęciami rozumia-
439030760.014.png 439030760.015.png 439030760.016.png 439030760.017.png
 
dla programistów
Programowanie równoległe z Qt
Program jako przykład
Rozważmy sytuację (często spotykana), że
jeden z wątków coś robi. Wynik swojej pracy
wkłada np. do kolejki (QQueue) o ograniczo-
nym rozmiarze. Ten wątek będziemy dalej na-
zywać Dostawcą. Na wyniki wykonanej pracy
czeka inny wątek, zwany dalej Odbiorcą, który
chce je wykorzystać do swoich celów. Odbior-
ca po pobraniu danej z początku kolejki, usu-
wa ją z niej. W języku wzorców projektowych
wątki można nazwać odpowiednio: Producer
i Consumer. W naszym przykładzie jeden wą-
tek będzie wkładał na koniec kolejki kolejne
liczby całkowite.
Musimy zapanować nad trzema sytuacja-
store_max_size ) { (2)
qDebug() << "> Dostawca: ide spac,
nie ma miejsca na moje dane";
Shared::store_not_full.wait(
&Shared::mutex ); (3)
}
Shared::store.enqueue( --d_value );
(4)
Shared::store_not_empty.wakeAll(); (5)
Shared::mutex.unlock(); (6)
}
const qint32 value = Shared::
store.dequeue(); (5)
qDebug() << "< Odbiorca: dostalem
liczbe: " << value;
Shared::store_not_full.wakeAll(); (6)
Jak widać, ta część kodu dostępu do kolejki
także chroniona jest muteksem (1).
Wywołanie konstruktora klasy QMutex-
Locker przełącza muteks w stan blokady (tak
jak mutex.lock() ) (1). Najpierw sprawdzamy
czy w kolejce są jakieś dane (2 ).
Aby zapobiec 'zawiśnięciu' programu po
skończeniu pracy przez Dostawcę, wprowa-
dziłem możliwość, dzięki której Odbiorca mo-
że sprawdzić czy Dostawca skończył swoja
pracę. Jeśli w kolejce nie ma danych i Dostawca
skończył pracę, to już nie ma na co czekać. Od-
biorca kończy działalność (3).
Jeśli jednak Dostawca pracuje, a danych
nie ma (czyli Odbiorca jest szybszy od Dostaw-
cy), przechodzimy w stan uśpienia (5) i cze-
kamy na sygnał aktywujący. Jeśli w kolejce są
jakieś dane, to je odczytujemy (5). No i na koń-
cu budzimy (ewentualnie) uśpiony wątek Dos-
tawcy (6). Na pierwszy rzut oka mogłoby się
wydawać, że zapomnieliśmy odblokować mu-
teks. Ale to nieprawda. O odblokowanie mu-
teksa zadba locker (QMutexLocker). Jest to
klasa, która w swoim konstruktorze bloku-
je muteks, a w destruktorze go odblokowuje.
Ponieważ locker został utworzony na stosie,
podczas wychodzenia (w dowolnym miej-
scu) z funkcji kończy sie zakres jego waż-
ności i automatycznie wywoływany jest je-
go destruktor. Jest to bardzo użyteczna kla-
sa, szczególnie gdy funkcja ma kilka miejsc
wyjścia (return). Normalnie przed każdym
z wyjść musielibyśmy ręcznie odblokowy-
wać muteks (sytuacja podatna na błędy zwią-
zane z nieuwagą programisty). Klasa Qmu-
texLocker i kompilator zrobią to za nas au-
tomatycznie.
Jak widać, kod chroniony jest muteksem (1)
i (6). Na początku aktywujemy muteks, zapew
niając sobie wyłączność na dostęp do kolej-
ki (1).
Następnie sprawdzamy czy kolejka jest
pełna (2). Jeśli tak właśnie jest, Dostawca nie
ma nic do roboty, nie ma miejsca na składo-
wanie kolejnych danych. W takiej sytuacji
Dostawcy nie pozostaje nic innego, niż przej-
ście w stan uśpienia i czekania na sygnał,
oznaczający że miejsce już jest (3). Następ-
nie (jeśli jest miejsce w kolejce) dodajemy na
koniec kolejki kolejną liczbę (4). W kolejnym
kroku budzimy (ewentualnie) uśpiony wątek
Odbiorcy (5). No i na koniec znosimy blo-
kadę, pozwalamy innym na dostęp do ko-
lejki (6).
Należy jednak zwrócić uwagę na jedna,
bardzo istotną rzecz. Po uaktywnieniu mu-
teksa (1), jeśli wątek przejdzie w stan uśpie-
nia (3) to mogłoby się wydawać, że program
„zawiśnie”. Przecież pozostał, wydawałoby
się że na zawsze, aktywny, blokujący mu-
teks. Tak oczywiście nie jest. Do zmiennej sto-
re_not_full (typu QWaitCondition ) przekazy-
wany jest adres muteksu. Właśnie po to, aby
został on odblokowany. Czyli wątek czeka
na sygnał pobudzający deaktywując muteks.
W momencie nadejścia sygnału i wyjścia
z funkcji wait() muteks będzie automaty-
cznie przywrócony do poprzedniego, czyli
blokującego, stanu.
Przyjrzyjmy się teraz kodowi Odbiorcy
(plik odbiorca.cpp, funkcja read):
mi:
• Dostawca i Odbiorca nie mogą pracować
z kolejką jednocześnie,
• Dostawca musi wiedzieć, że w kolejce jest
wolne miejsce (jest gdzie składować dane),
• Odbiorca musi wiedzieć, że w kolejce coś
jest (jest coś do odebrania)
Dostawca i Odbiorca nie mogą pracować jed-
nocześnie z kolejka. W tym celu zastosujemy
klasę Qmutex. Dostawca musi być powiado-
miony, że w kolejce jest wolne miejsce, a Od-
biorca że jest coś do odebrania. Oczywiście
mogliby, każdy w swojej pętli, sprawdzać cały
czas stan kolejki, ale obciążałoby to w znaczący
sposób procesor, można by zaobserwować spo-
re (lub nawet drastyczne) spowolnienie pracy
programu. Rozwiązaniem jest uśpienie wątku,
który aktualnie nie może wykonywać swojej
pracy. Każdy z wątków powinien zostać uak-
tywniony dopiero wtedy, gdy zajdą warunki
sprzyjające wykonywaniu przez niego pracy.
Klasa która pozwala usypiać wątki i budzić je
jeśli zajdzie określony warunek to QWaitCon-
dition.
UWAGA: Ponieważ musimy zsynchroni-
zować dwie różne klasy-wątki, zmienne typu
QMutex i QWaitCondition nie mogą należeć
do żadnej z nich. Teoretycznie mogłyby być
globalne, ale jako zdecydowany przeciwnik
zmiennych globalnych umieszczę je w osobnej
klasie jako zmienne statyczne.
W celach prezentacji współdziałania wąt-
ków, do artykułu dołączono przykładowy pro-
gram o nazwie watki (projekt watki.pro). Moż-
na go uruchomić na swoim komputerze i po-
obserwować jak wątki ze sobą współpracują.
Tutaj chciałbym tylko omówić najbardziej isto-
tne elementy programu. Przeanalizujmy kod
Dostawcy (plik dostawca.cpp, funkcja save):
Podsumowanie
Jak widać używanie muteksów nie jest aż
takie skomplikowane. Mam nadzieję, że czy-
telnik po lekturze tego artykułu, podzieli ze
mną ten pogląd. Od programisty implemen-
tacja wątków wymaga pewnego dodatko-
wego nakładu pracy. Wzmożonej uwagi i kon-
centracji. Jednak pod względem koncep-
cyjnym używanie wątków nie jest zbyt trud-
ne. A nakład pracy może zwrócić się z na-
wiązką.
QMutexLocker locker( &Shared::mutex
); (1)
if( Shared::store.isEmpty() ) { (2)
if( Shared::is_inished() ) {
...
return; (3)
}
else {
qDebug() << "< Odbiorca: ide
spac, brak danych";
Shared::store_not_empty.wait(
&Shared::mutex ); (4)
}
Shared::mutex.lock(); (1)
if( Shared::store.size() == Shared::
www.lpmagazine.org
67
439030760.001.png 439030760.002.png 439030760.003.png 439030760.004.png 439030760.005.png 439030760.006.png 439030760.007.png 439030760.008.png 439030760.009.png 439030760.010.png 439030760.011.png 439030760.012.png 439030760.013.png
 
Zgłoś jeśli naruszono regulamin