2007 07 Programowanie aplikacji wielowątkowych w języku C w oparciu o wzorce projektowe [Inzynieria Oprogramowania]

background image

58

Inżynieria

oprogramowania

www.sdjournal.org

Software Developer’s Journal 07/2007

Programowanie aplikacji wielowątkowych

w języku C++ w oparciu o wzorce projektowe

W

ytwarzanie oprogramowania wielowątko-

wego obrosło legendą problemu bardzo

trudnego, co powoduje, że pisanie aplika-

cji wielowątkowych przyprawia o dreszcz grozy wie-

lu programistów. Jest to poniekąd prawda, lecz wyda-

je się, iż odpowiednie rozparcelowanie systemu oraz

usystematyzowanie pewnych pojęć może w znacz-

nym stopniu rozwiać takie obawy. Główna przewaga

programów wielowątkowych, uruchamianych na poje-

dynczym procesorze, nad programami jednowątkowy-

mi skupia się w obszarze optymalizacji dostępu do za-

sobów i nie daje sama w sobie dodatkowych korzyści,

a wręcz powoduje obniżenie wydajności systemu z ra-

cji potrzeby konsumpcji czasu procesora na przełącza-

nie kontekstu wątków, co nie występuje przy przetwa-

rzaniu jednowątkowym [Alex01]. Z drugiej strony opty-

malizowanie dostępu do zasobów, czy wytwarzanie

responsywnego interfejsu użytkownika jest dużo ła-

twiejsze i bardziej eleganckie, gdy dysponujemy moż-

liwością uruchomienia aplikacji w wielu wątkach prze-

twarzania, poniekąd wymuszając na nas taką a nie in-

ną decyzję odnośnie ilości wątków w systemie.

W systemie jednoprocesorowym odpowiedzial-

nością za zarządzanie wątkami obarczone jest jądro

systemu operacyjnego. Ponieważ często architektu-

ry prymitywnych procesorów nie zapewniają takiego

wsparcia dla systemu operacyjnego, wsparcie to za-

pewnia kompilator wstrzykując odpowiednie instruk-

cje w trakcie generowania kodu wykonywalnego. In-

strukcje te oznaczają miejsca, w których jeden wątek

może przełączyć się na inny. Sam proces przełącza-

nia wątków wymaga oczywiście strategii w ramach

której nastąpi decyzja o tym, który wątek powinien

otrzymać sterowanie, co samo w sobie nie jest zada-

niem trywialnym. Omówiona wyżej operacja wstrzyki-

wania instrukcji nazywana jest podziałem czasu (ang.
time slicing) procesora. Pozwala ona na emulację

wsparcia, które już bezpośrednio (czyt. sprzętowo)

zrealizowane jest w bardziej zaawansowanych archi-

tekturach procesorów.

Zarówno standard C jak i C++, są bardzo

oszczędne jeśli chodzi o określenie zachowania

się języka w aplikacji wielowątkowej, wyrażając się

w słowie kluczowym

volatile

i przerzucając niejako

na programistę zagadnienia związane z synchroni-

zacją takich aplikacji.

Daje to początkującemu programiście tak wielki

wachlarz możliwości, że bardzo łatwo może on po-

pełnić podstawowe błędy, które w późniejszym eta-

pie projektu mogą doprowadzić do zakleszczeń, wy-

ścigów, rozsynchonizowań i innych bardzo trudnych

do namierzenia i wyeliminowania błędów. Głównym

sprzymierzeńcem w walce z tym zjawiskiem okazują

się sprawdzone i dogłębnie przeanalizowane rozwią-

zania konstrukcyjne znane jako wielowątkowe wzor-

ce projektowe. Można napisać, że jednym z kluczo-

wych czynników decydujących o sukcesie projektu

programistycznego pisanego z użyciem wielowąt-

kowości jest zastosowanie odpowiednich konstrukcji

podstawowych oraz technik parcelacji systemu.

Pojęcia podstawowe

Słowo kluczowe

volatile

(pol. ulotny), podobnie jak

const

jest modyfikatorem typu, jednak w dziedzinie

jego zastosowania leżą zmienne, które mogą być do-

stępne z poziomu różnych wątków, różnych włókien

czy też mogą zostać zmodyfikowane w czasie obsłu-

gi przerwań. Słowo to zastosowane do zmiennej, in-

struuje kompilator o tym, że zmienna jest ulotna, czy-

li że jej wartość może być zmieniana w sposób in-

ny niż to wynika z drzewa wywołań danej procedu-

ry, a co za tym idzie, kompilator wyłącza na jej rzecz

wszelkiego rodzaju optymalizacje powodujące bufo-

rowanie-cacheowanie tej zmiennej, które to optyma-

lizacje mogłyby spowodować, iż jej wartość byłaby

widziana różnie z różnych kontekstów mających do

niej dostęp. Inaczej mówiąc, dla zmiennej zadeklaro-

wanej z modyfikatorem typu volatile, kompilator po-

winien zawsze zapewnić fizyczny odczyt jej wartości

z pamięci, niezależnie od kontekstu, w którym dany

odczyt ma miejsce. Dla przykładu rozważmy nastę-

pującą pętlę wykonywaną przez wybrany wątek:

flag=false;
while (!flag)
{
Sleep(1000); // sleeps for 1000 milliseconds
}

Inny wątek lub funkcja obsługi przerwania w pew-

nym momencie ustawia tą zmienną na wartość

true

,

co w zamierzeniu ma wymusić na pierwszym wątku

opuszczenie pętli

while

. Kompilator widząc powyż-

szy kod, nie wie jednak jaki jest zamysł programisty

i cel użycia tej zmiennej, więc chcąc zoptymalizować

kod wynikowy ruguje ją, zastępując powyższą pę-

tlę – pętlą nieskończoną, przez co zachowanie takiej

aplikacji jest w oczywisty sposób błędne. Wyjściem

Paweł Kapłański

Autor jest architektem wiodącym w Samsung Electro-
nics Poland R&D Center, gdzie zajmuje się m.in. syste-
mami wbudowanymi (ang. embedded).
Kontakt z autorem: pawel.kaplanski@wp.pl

background image

Programowanie aplikacji wielowątkowych w C++

59

www.sdjournal.org

Software Developer’s Journal 07/2007

z tej sytuacji jest zastosowanie modyfikatora

volatile

do

zmiennej

flag

, co daje kompilatorowi wiedzę o charakterze

tej zmiennej. Bez słowa kluczowego

volatile

pisanie aplika-

cji wielowątkowych byłoby niemożliwe lub też kompilator byłby

pozbawiony możliwości zaawansowanych technik optymaliza-

cji kodu wynikowego.

Warto wspomnieć, że słowo kluczowe

volatile

może być sto-

sowane, podobnie jak słowo kluczowe

const

, zarówno do typów

prostych jak i złożonych. W przypadku typów złożonych podob-

nie jak kwalifikator

const

modyfikuje wszystkie typy składowych

obiektu złożonego. Użycie tego słowa kluczowego w stosunku do

typów złożonych daje zaskakujące wręcz możliwości podniesie-

nia jakości procesu wytwarzania aplikacji wielowątkowych w języ-

ku C++ o czym można przeczytać w pracy [Alex03]. Ignorowanie

natomiast słowa kluczowego

volatile

bez zrozumienia jego prze-

znaczenia może owocować bardzo dziwnymi i ciężkimi w detekcji

błędami, takimi jak różnica w działaniu programu wielowątkowe-

go w wersji developerskiej – z włączonymi mechanizmami debu-

gującymi, w stosunku do wersji finalnej (Release), w której to włą-

cza się całe spektrum optymalizacji kodu.

Wydawać by się mogło, że odpowiednie używanie

vola-

tile

daje nam możliwość zabezpieczenia się przed baga-

żem niebezpieczeństw sponsorowanych przez architekturę

procesora czy optymalizacje kompilatora. Niestety, progra-

mista aplikacji wielowątkowych, mając do czynienia z nowo-

czesnymi architekturami mikroprocesorów, musi zdawać so-

bie sprawę również z możliwością zachodzenia zjawiska, na-

zywanego: nieuporządkowanym dostępem do pamięci (ang.
memory out-of-ordering
). Zjawisko to rozwiązywane jest za

pomocą mechanizmu (ang. memory-barier), które nakazu-

je procesorowi/procesorom wymuszenie uporządkowania do-

stępu do pamięci na granicy tejże bariery. Niestety język C jak

i C++ ze względu na brak standardu w kwestii wątków oraz

bardzo pobieżne potraktowanie tematu wielowątkowości nie

daje nam żadnego wsparcia powodując to, że aby użyć barier

pamięci musimy posłużyć się kodem asemblerowym. W prze-

ciwieństwie do słowa

volatile

, wysokopoziomowe koncepcje

Listing 1.

Singleton nie zabezpieczony przed wyścigami

template

<

class

T

>

class

Singleton

{

private

:

static

T

*

impl

;

public

:

static

T

&

instance

()

{

if

(

instance_

==

0

)

{

instance_

=

new

T

;

std

::

atexit

(&

Singleton

<

T

>::

release

);

}

return

*

instance_

;

}

Listing 2.

Singleton z sekcją krytyczną

#include

<boost/thread/mutex.hpp>

template

<

class

T

>

class

Singleton

{

private

:

struct

Instance

{

Instance

()

:

instance_

(

0

)

{}

T

*

instance_

;

boost

::

mutex

mutex_

;

}

;

static

Instance

impl

;

public

:

static

T

&

instance

()

{

// poczatek sekcji krytycznej

boost

::

mutex

::

scoped_lock

lock

(

impl

.

mutex_

);

if

(

impl

.

instance_

==

0

)

{

impl

.

instance_

=

new

T

;

std

::

atexit

(&

Singleton

<

T

>::

release

);

}

return

*

impl

.

instance_

;

}

// koniec sekcji krytycznej

}

;

Listing 3.

Wyjście z tej sytuacji opisuje wzorzec

Podwójnie Sprawdzonego Blokowania

Singleton

&

Singleton

::

Instance

()

{

if

(!

instance_

)

// 1

{

// 2

boost

::

mutex

::

scoped_lock

lock

(

mutex_

);

if

(!

instance_

)

// 4

{

instance_

=

new

Singleton

;

}

}

return

*

instance_

;

}

Rysunek 1.

Składowe wzorca Monitor

������

�������

��������������
��������������

���������

������
��������

�����

��������
���������

����

����

����

background image

60

Inżynieria

oprogramowania

www.sdjournal.org

Software Developer’s Journal 07/2007

wielowątkowe takie jak Thread, Mutex, ScopedLocking idiom,

wzorzec Doble-Checked Locking czy obiekt Condition umożli-

wiają parcelację systemu. Jako, że biblioteka standardowa nie

udostępnia mechanizmów synchronizacyjnych, posłużymy się

w tym artykule biblioteką boost (http://www.boost.org). Wyko-

rzystamy obiekty

boost::mutex

,

boost::condition

i

boost::thre-

ad

oraz użyjemy oferowanych przez nią sprytnych wskaźników

z licznikiem referencji (

boost::shared _ ptr

).

Zagadnienia synchronizacyjne

Klasycznym problemem, z którym styka się każdy progra-

mista zajmujący się wytwarzaniem oprogramowania wielo-

wątkowego, jest problem szeregowania dostępu do zaso-

bów współdzielonych pomiędzy różnymi wątkami. Ten pro-

blem rozwiązywany jest zazwyczaj za pomocą koncepcji Sek-

cji Krytycznej (ang. Critical Section). Sekcje krytyczne to wy-

różnione zbiory procedur, takich że tylko jedna z nich może

być wykonywana w danej chwili. Jeśli jedna procedura z tego

zbioru jest właśnie wykonywana i inny wątek próbuje wykonać

dowolną procedurę z tego zbioru, wówczas musi on zaczekać,

aż pierwsza procedura zakończy swoje działanie [Abel96].

Sekcje krytyczne realizowane są zazwyczaj za pomocą

obiektu Mutex (ang. Mutual Exclusion), który to obiekt może być

tylko w dwóch rozłącznych stanach: zajęty (ang. aquire) lub zwol-

niony (ang. release). Pojedynczy wątek może blokować Mutex

dowolną ilość razy, lecz pozostałe wątki chcąc zająć Mutex mu-

Listing 5.

Operatory wyłuskiwania instancji

template

<

class

T

>

class

Singleton

{

...

T

*

operator

->()

{

return

&

instance

();

}

T

&

operator

*()

{

return

instance

();

}

...

}

;

Listing 6.

Element Kolejki Aktywacji

class

ActivationQueueNode

{

public

:

ActivationQueueNode

():

_prev

(

0

)

{}

static

void

*

operator

new

(

size_t

size

,

ActivationQueue

&

queue

);

static

void

operator

delete

(

void

*

,

ActivationQueue

&)

{

assert

(

false

);

}

protected

:

template

<

class

T

>

friend

void

destroy

(

T

*

p

,

ActivationQueue

&

a

);

friend

class

ActivationQueue

;

virtual

~

ActivationQueueNode

()

{}

ActivationQueueNode

*

_prev

;

}

;

Listing 7.

Kolejka Aktywacji

class

ActivationQueue

{

public

:

explicit

ActivationQueue

(

size_t

maxSize

=

0

);

void

setupEventSize

(

size_t

eventSize

);

ActivationQueueNode

*

peek

();

void

push

(

ActivationQueueNode

*

pev

);

private

:

void

*

alloc

(

size_t

size

);

void

free

(

void

*

memNode

);

}

;

template

<

class

T

>

void

destroy

(

T

*

p

,

ActivationQueue

&

queue

)

{

if

(

p

)

{

// explicit destructor call

p

->

~

T

();

queue

.

free

(

p

);

}

}

inline

void

*

ActivationQueueNode

::

operator

new

(

size_t

size

,

ActivationQueue

&

queue

)

{

return

queue

.

alloc

(

size

);

}

Listing 4.

Dobule-Checked Locking Pattern

template

<

class

T

>

class

Singleton

{

...

public

:

static

T

&

instance

()

{

if

(

impl

.

instance_

==

0

)

{

boost

::

mutex

::

scoped_lock

lock

(

impl

.

mutex_

);

if

(

impl

.

instance_

==

0

)

{

impl

.

instance_

=

new

T

;

std

::

atexit

(&

Singleton

<

T

>::

release

);

}

}

return

*

impl

.

instance_

;

}

}

;

background image

62

Inżynieria

oprogramowania

www.sdjournal.org

Software Developer’s Journal 07/2007

szą poczekać aż wątek blokujący zwolni dany Mutex całkowicie

(tyle razy ile sam go zajął). Gdy to nastąpi, inny wątek może zająć

Mutex tym samym zabierając tą możliwość innym wątkom.

Para akcji zajmij-zwolnij, która odbywa się na obiekcie Mu-

teksa zazwyczaj ma zasięg lokalny i jest realizowana za po-

mocą paradygmatu blokowania zakresu (ang. scoped locking)

[Schm98], odbywającego się przy użyciu zmiennych auto-

matycznych, które w konstruktorze blokują, a w destruktorze

zwalniają wybrany Mutex.

Drugim w kolejności problemem, z którym w głównej mie-

rze spotykamy się w zagadnieniach z pogranicza baz danych,

jest problem czytelników i pisarzy. Dotyka on takich zasobów do

których sposoby dostępu możemy podzielić na dwie grupy, do-

stęp tylko do odczytu i dostęp w celu modyfikacji. Wątek chcą-

cy uzyskać dostęp tylko do odczytu nazywany jest czytelnikiem,

natomiast wątek chcący otrzymać dostęp w celu zmodyfikowa-

nia zawartości zasobu – pisarzem. Pisarz jest jednocześnie czy-

telnikiem. Czytelnicy nie powinni się wzajemnie blokować, lecz

pisarz powinien zaczekać aż czytelnicy przestaną czytać i do-

piero wówczas zająć zasób aby coś „napisać”. Po zajęciu zaso-

bu przez pisarza żaden czytelnik ani inny pisarz nie może zająć

tego zasobu jednocześnie, a co za tym idzie, taka sytuacja jest

analogiczna do Sekcji Krytycznej. Dopiero po zwolnieniu zaso-

bu przez pisarza, inny pisarz lub czytelnicy mogą zająć zasób.

Rysunek 2.

Działanie elementów składowych wzorca Monitor

�������

�������

�������

�����

���������

������

����

������

�����������

����������������

�������

������

�����������

�������

�������

�������

�������

����������������

����������������

Rysunek 3.

Składowe wzorca Active Object

�����

����������
����������

���������

����������
��������

����������������

������
�����

�������

������

����������������

���������

�������

����������
����������

���������

���������

���������

���������

������������

���������������

������������

�����������

����������

����������������������

�������������

background image

63

Programowanie aplikacji wielowątkowych w C++

www.sdjournal.org

Software Developer’s Journal 07/2007

Okazuje się, że w problemie czytelników i pisarzy bardzo trudno

jest zapewnić „sprawiedliwość” dostępu pomiędzy czytelnikami

i pisarzami, tak aby nie dochodziło do zagłodzenia jednych

przez drugich. Problem czytelników i pisarzy rozwiązywany jest

zazwyczaj za pomocą obiektu RWMutex (ang. Read Write Mu-
tex
), a sprawiedliwe rozwiązanie problemu czytelników i pisarzy,

które nie faworyzuje ani jednych ani drugich zostało opisane do-

piero na końcu lat dziewięćdziesiątych w pracy [Sanl99]. Roz-

wiązanie to jednak, z uwagi na ilość występujących w nim kon-

strukcji synchronizacyjnych, w „standardowych” przypadkach

nie jest implementowane, na rzecz prostszych i szybszych, acz

niesprawiedliwych rozwiązań.

Bezpieczny Singleton

Bardzo często zachodzi potrzeba eliminacji wyścigów w pro-

cesie inicjalizacji obiektu dostępnego przez różne wątki. Naj-

lepszym przykładem jest tutaj implementacja wielowątkowe-

go wzorca Singleton, która w optymalnej wersji jest realizowa-

na z użyciem wzorca Podwójnie Sprawdzanego Blokowania

(ang. Double-Checked Locking Pattern) [Schm96]. Wzorzec

ten rozwiązuje problem atomowej inicjalizacji obiektów, któ-

re istnieją przez cały czas życia systemu od momentu swo-

jego poczęcia. Wzorzec Singleton [Gamm95] opisuje z kolei

obiekt istniejący przez całe życie systemu w dokładnie jed-

nej kopii. Jego stworzenie jest zazwyczaj odraczane do cza-

su pierwszego użycia, jednak w aplikacjach wielowątkowych

może dochodzić, w trakcie takiego pierwszego odwołania, do

wyścigów między rywalizującymi ze sobą wątkami. Na Listin-

gu 1. ukazana jest taka niebezpieczna, ze względu na wielo-

wątkowość, implementacja wzorca Singleton w formie szablo-

nu. Warto tu zwrócić uwagę na użycie funkcji

std::atexit

, któ-

ra rejestruje funkcję niszczącą Singleton w momencie zamy-

kania systemu (kończenia wykonania programu).

Aby wyeliminować wyścigi w pierwszym podejściu zasto-

sujemy Sekcję Krytyczną opartą na Muteksie, wynikiem tego

zabiegu jest kod znajdujący się na Listingu 2.

Każdorazowy dostęp do instancji Singletona używa me-

chanizmów systemowych – w tym przypadku blokowanie

i zwalnianie Muteksa. Może to okazać się bardzo kosztowne

przy częstym dostępie do Singletonu.

Zasięg wyścigów dotyka tutaj tylko sprawdzenia czy obiekt

Singleton został, czy też nie został stworzony. Jeśli obiekt został

wcześniej stworzony to, po prostu, zwracamy ten obiekt. Sam

proces tworzenia jest już jednak chroniony sekcją krytyczną,

a zapewnione to jest poprzez ponowne sprawdzenie czy obiekt

nie został przypadkiem stworzony. Tylko w przypadku gdy bę-

dąc w sekcji krytycznej wątek, który ją zawłaszczył, stwierdza

że Singleton jeszcze nie jest stworzony, tylko wtedy ten sam wą-

tek go tworzy. W czasie gdy wybrany wątek tworzył Singleton,

pozostałe wątki mogły czekać na wpuszczenie do sekcji kry-

Listing 8.

Schemat użycia kolejki

void

push

()

{

boost

::

mutex

::

scoped_lock

lock

(

mutex_

);

ActivationQueueNode

*

node

=

new

(

queue_

)

ActivationQueueNode

;

// wypelnij element przed wstawieniem do kolejki

while

(

node

==

0

)

{

notFull

.

wait

(

lock

);

node

=

new

(

queue_

)

ActivationQueueNode

;

}

queue_

.

push

(

node

);

notEmpty

.

notify_all

();

}

void

pop

()

{

boost

::

mutex

::

scoped_lock

lock

(

mutex_

);

ActivationQueueNode

*

node

=

queue_

.

pop

();

while

(

node

==

0

)

{

notEmpty

.

wait

(

lock

);

node

=

queue_

.

pop

();

}

// przetworz element przed jego zwonieniem

destroy

(

node

,

queue

);

notFull

.

notify_all

();

}

Listing 9.

Abstrakcja Komendy

class

ICommandDelegateProxy

:

public

ActivationQueueNode

{

public

:

virtual

void

operator

()()

=

0

;

}

;

Listing 10.

Generyczna lista argumentów komendy

template

<

typename

Arg1

,

typename

Arg2

, ...

>

struct

ArgList

<

Arg1

,

Arg2

,

Arg3

,

Arg4

>

{

ArgList

(

const

Arg1

&

theArg1

,

const

Arg2

&

theArg2

,

...

)

:

arg1

(

theArg1

)

,

arg2

(

theArg2

)

,

...

{}

Arg1

arg1

;

Arg2

arg2

;

...

}

;

Listing 11.

Wywołanie funktora z listą argumentów

template

<

typename

Caller

,

typename

Arg1

,

typename

Arg2

,...

>

inline

void

CallWithArgs

(

Caller

&

caller

,

ArgList

<

Arg1

,

Arg2

,...

>&

args

)

{

caller

(

args

.

arg1

,

args

.

arg2

,...

);

}

background image

64

Inżynieria

oprogramowania

www.sdjournal.org

Software Developer’s Journal 07/2007

tycznej, gdy tylko wątek-twórca opuści sekcję krytyczną, dowol-

ny wątek oczekujący sprawdzi, że Singleton został stworzony

i opuści natychmiast sekcję krytyczną.

Niestety okazuje się, że w rzeczywistości możemy, pomi-

mo prostoty tego wzorca, natknąć się na problemy związane

z barierami pamięci, o których wspomniałem we wstępie, dlate-

go należy być bardzo ostrożnym i rozważając użycie tego wzor-

ca dokładnie o nim poczytać w [Alex01] lub [Schm00].

Na zakończenie warto wzbogacić implementację Singleto-

na w odpowiednie operatory

->

oraz

*

. Umożliwi to używanie

Singletonów podobnie jak proxy-wskaźników, zaoszczędzając

sporo niepotrzebnej „pisaniny”.

Mutex + Condition = Semaphore

Inną gałęzią zagadnień są problemy komunikacji wielowąt-

kowej. Dotyczą one bardzo szeroko rozumianej wymiany in-

formacji pomiędzy wieloma wątkami działającymi jednocze-

śnie. Podstawowe rozwiązania tego problemu są budowane

na bazie Muteksu, obiektu Condition oraz kolejki komunika-

tów. Wzorce oparte na tych trzech mechanizmach podstawo-

wych mają w zamierzeniu zapewnić maksimum bezpieczeń-

stwa poprzez eliminacje, na ile to tylko możliwe, zakleszczeń

(ang. deadlock) oraz wyścigów (ang. race condition) już na po-

ziomie projektu systemu.

Obiekt Conditio, podobnie jak Mutex, jest współdzielony po-

między wątkami działającymi jednocześnie i umożliwia syn-

chronizację działań tych wątków. Obiekt Condition używany jest

w parze z pewnym obiektem typu Mutex. Mutex służy do wy-

tworzenia sekcji krytycznej w której ramach działa obiekt Condi-

tion i w związku z tym musi być zajęty w momencie, gdy docho-

dzi do oczekiwania na obiekcie Condition, co realizowane jest po-

przez podawanie do funkcji

wait

obiektu Condition odpowiednie-

go obiektu automatycznego typu Lock, realizującego blokowa-

nie zakresu w odniesieniu do obiektu Mutex. Po zawieszeniu się

na obiekcie Condition, wątek czeka aż inny wątek zanotyfiku-

je współdzielony obiekt Condition. Rozpoczynając oczekiwanie

na obiekcie Condition – Mutex zostaje automatycznie zwolnio-

ny, umożliwiając innym wątkom zajęcie sekcji krytycznej. Inny wą-

Rysunek 4.

Współdziałanie składowych wzorca ActiveObject

��������

�������

���������

�����������

������������

�����

�����������

���������

������������

�������������

��������

��������������

����

������

��������������������������������������

������

���

�������

���������������������

Listing 12.

Implementacja komendy

template

<

typename

Arg1

, ...

>

class

CommandDelegateProxyImpl

:

public

ICommandDelegateProxy

{

public

:

explicit

CommandDelegateProxyImpl

(

const

ArgList

<

Arg1

,

Arg2

,...

>&

args

,

boost

::

shared_ptr

<

Event

<

void

,

Arg1

,

Arg2

,...

>

>

eventGo

):

args_

(

args

)

,

eventGo

(

eventGo

)

{}

virtual

void

operator

()()

{

CallWithArgs

(*

eventGo_

,

args_

);

}

private

:

ArgList

<

Arg1

,

Arg2

,

Arg3

,

Arg4

,

Arg5

>

args_

;

boost

::

shared_ptr

<

Event

<

void

,

Arg1

,

Arg2

,

Arg3

,

Arg4

,

Arg5

>

>

eventGo_

;

}

;

Listing 13.

Algorytm „pobierz, przetwórz, zwolnij”

inline

bool

PeekAndProcess

(

ActivationQueue

*

queue

,

boost

::

mutex

::

scoped_lock

&

lock

,

boost

::

condition

&

notFull

)

{

if

(

ICommandDelegateProxy

*

cmd

=

static_cast

<

ICommandDelegateProxy

*>(

queue

->

peek

()))

{

{

AntyLock

antyLock

(

lock

);

(*

cmd

)();

}

destroy

(

cmd

,

*

queue

);

notFull

.

notify_all

();

return

true

;

}

return

false

;

}

background image

65

Programowanie aplikacji wielowątkowych w C++

www.sdjournal.org

Software Developer’s Journal 07/2007

Listing 14.

Genryczne Proxy

tek działając w obrębie tej właśnie sekcji krytycznej ma możliwość

notyfikacji obiektu Conditio, co spowoduje ponowne zajęcie Mu-

teksa przez wątek oczekujący i zakończenie oczekiwania.

Za pomocą obiektu Condition i Mutex możemy zapisać do-

wolne zagadnienie synchronizacyjne, gdyż okazuje się, że są

one równoważne semaforom Dijkstry. Z kolei Condition i Mutex

może być zapisany przy użyciu semaforów, więc oba mechani-

zmy są równoważne. W praktyce realizuje się obiekt Condition

i Mutex jako twory wysokiego poziomu, pozostawiając na niskim

poziomie – bezpośrednio wspieranym przez system operacyjny

– konstrukcje semaforów, jako prymitywy podstawowe.

Wzorzec Monitor

Wzorzec Monitor [Schm00] nazywany również PassiveObject,

jest modelem obiektu, którego właściwością podstawową

jest to, że nie będzie uruchomiona na raz więcej niż jedna je-

go metoda. Można to zrealizować w prosty sposób, blokując

każdą z metod publicznych obiektu Monitor jednym wspólnym

Muteksem. Sam w sobie schemat działania Monitora rozsze-

rza jednak obiekt Condition, dodając do Monitora funkcjonal-

ności mediacyjne. Używając wyłącznie Muteksu nie byłoby

możliwe stworzenie modelu aktywnego oczekiwania na zaj-

ście pewnego zdarzenia. O zajściu tego zdarzenia można by

się dowiedzieć jedynie poprzez aktywne odpytywanie (ang.
pooling
), odpowiednich metod. Obiekt Condition daje nam

możliwość optymalnego oczekiwania na zdarzenie.

Monitor jako wzorzec opisuje, w jaki sposób można dzielić po-

jedynczy obiekt pomiędzy wiele wątków chcących uzyskać do

niego dostęp poprzez wywołanie na nim metod lub oczekiwa-

nie na odpowiednie komunikaty płynące z tego obiektu. Z drugiej

strony można patrzeć na wzorzec Monitor jako na wielowątkowe-

go Mediatora, pozwalającego różnym obiektom na komunikację,

a obiekt zrealizowany za jego pomocą można postrzegać jak im-

plementację pewnego protokołu komunikacji miedzyobiektowej.

Wzorzec ActiveObject

Nazwa PassiveObject oddaje naturę wzorca Monitor, gdyż
Monitor nie zarządza wątkami, lecz raczej używa wątków

stworzonych na zewnątrz, w odróżnieniu od wzorca Active-
Object
[Schm00], który rozwiązuje problem dostępu do za-

sobów, separując dostęp do swoich funkcjonalności za po-

mocą wielowątkowego interfejsu typu Proxy. Klient uzysku-

je dostęp poprzez wyżej wymienione Proxy, tworząc Sesję

(ang. Session) i poprzez Proxy, które generuje odpowied-

nią Komendę (ang. Command) przekazywaną do Kolejki

(ang. ActivationQueue) w której oczekuje ona na obsługę

przez wątek roboczy Schedulera wzorca ActiveObject. Sesja,

realizując wzorzec Monitor, umożliwia klientowi oczekiwanie

na zakończenie pracy nad wydaną Komendą.

template

<

typename

Arg1

,

typename

Arg2

,...

>

class

CommandProxy

{

public

:

CommandProxy

()

:

eventGo_

(

new

Event

<

void

,

Arg1

,

Arg2

,

Arg3

,

Arg4

,

Arg5

>)

{}

void

operator

+=

(

EventDelegate

<

void

,

Arg1

,

Arg2

,

Arg3

,

Arg4

,

Arg5

>

theMethod

)

{

(*

eventGo_

)+=

theMethod

;

}

void

operator

-=

(

EventDelegate

<

void

,

Arg1

,

Arg2

,

Arg3

,

Arg4

,

Arg5

>

theMethod

)

{

(*

eventGo_

)-=

theMethod

;

}

void

setSync

(

boost

::

shared_ptr

<

ActivationQueue

>

queue

,

boost

::

shared_ptr

<

boost

::

mutex

>

mutex

,

boost

::

shared_ptr

<

boost

::

condition

>

notEmpty

,

boost

::

shared_ptr

<

boost

::

condition

>

notFull

)

{

queue_

=

queue

;

notEmpty_

=

notEmpty

;

notFull_

=

notFull

;

mutex_

=

mutex

;

}

Listing 15.

Interfejs przykładowego ActiveObject

class

ActiveObject

{

public

:

ActiveObject

();

void

start

();

void

stop

();

CommandProxy

<

int

>

doSomething

;

private

:

boost

::

shared_ptr

<

ActiveObjectImpl

>

impl_

;

}

;

private

:

boost

::

shared_ptr

<

Event

<

void

,

Arg1

,

Arg2

,

Arg3

,

Arg4

,

Arg5

>

>

eventGo_

;

boost

::

shared_ptr

<

ActivationQueue

>

queue_

;

boost

::

shared_ptr

<

boost

::

mutex

>

mutex_

;

boost

::

shared_ptr

<

boost

::

condition

>

notEmpty_

;

boost

::

shared_ptr

<

boost

::

condition

>

notFull_

;

public

:

void

operator

()

(

Arg1

arg1

,

Arg2

arg2

, ...

)

{

{

boost

::

mutex

::

scoped_lock

lock

(*

mutex_

);

ActivationQueueNode

*

n

=

0

;

while

(

(

n

=

new

(*

queue_

)

CommandDelegateProxyImpl

<

Arg1

,

Arg2

,

Arg3

,

Arg4

,

Arg5

> (

ArgList

<

Arg1

,

Arg2

,

Arg3

,

Arg4

,

Arg5

>(

arg1

,

arg2

,

arg3

,

arg4

,

arg5

)

,

eventGo_

))==

0

)

notFull_

->

wait

(

lock

);

queue_

->

push

(

n

);

notEmpty_

->

notify_all

();

}

}

}

;

background image

66

Inżynieria

oprogramowania

www.sdjournal.org

Software Developer’s Journal 07/2007

Z życia wziętym przykładem, w jaki możemy sobie wyobra-

żać mechanizmy działające wewnątrz ActiveObject'u jest re-

stauracja. Klient zamawiając posiłek, wydaje komendę kelnero-

wi, który odpowiada Komendzie, kelner zgłasza zamówienie do

kuchni, wpisując je na listę zamówień, do której wgląd ma szef

kuchni. Lista zamówień odpowiada tutaj Kolejce. Szef kuch-

ni realizuje zamówienia dzieląc je na odpowiednie etapy pracy.

Gdy danie jest gotowe kelner zanosi je do klienta. Może się tak

zdarzyć że zamawiając proste danie wybrany klient otrzyma je

szybciej od klienta, który zamówił danie bardziej skomplikowa-

ne, a zamówił je dużo wcześniej. Z punktu widzenia szefa kuch-

ni nie ma to znaczenia, jego praca polega na krojeniu, smaże-

niu, gotowaniu i innych małych czynnościach. Dzięki temu re-

stauracja może obsłużyć wielu klientów optymalizując dostęp

do cennego zasobu jakim jest szef kuchni poprzez zmaksyma-

lizowanie jego efektywnego czasu pracy (np. czekając na usma-

żenie frytek dla jednego klienta kucharz może przyrządzać sa-

łatkę dla innego). Z punktu widzenia klienta kelner przyjmu-

je zamówienie i dostarcza posiłek, nie wiedząc nawet tego co

w tym okresie czasu działo się w kuchni. Wzorce Monitor i Acti-

veObject wyodrębniają wiele podstawowych konstrukcji:

Mutex

– postawowy mechanizm umożliwiający tworzenie

Sekcji Krytycznych;

Condition

– podstawowy mechanizm komunikacyjny, skła-

dowa wzorca Monitor;

ActivationQueue

– kolejka aktywacji serwera;

Command

– komenda asynchroniczna zlecana przez wątek

kliencki do innego wątku;

CommandProxy

– proxy komendy asynchronicznej.

Dysponując powyższym zbiorem mechanizmów podstawo-

wych w formie generycznej otrzymujemy potężne narzędzie

pozwalające na parcelację systemów wielowątkowych. Mutex

oraz Condition jako konstrukcje podstawowe zapożyczyliśmy

z biblioteki BOOST (www.boost.org). Zajmijmy się kolejką ak-

tywacji. Można użyć dowolnej implementacji kolejki np. kolejki

z biblioteki standardowej, lecz poniższa implementacja będzie

miała taką właściwość, że alokacja pamięci będzie występo-

wała jedynie przy pierwszym użyciu. W tym celu przeciążymy

operatory

new

oraz

delete

obiektu

ActivationQueueNode

.

Kolejka komunikatów będzie jednocześnie alokatorem dla

swoich elementów i jako taka będzie podawana do przecią-

żonych operatorów. Kolejka w konstruktorze ma podawaną

maksymalną ilość elementów, gdy ta liczba jest równa zeru,

wówczas ograniczenie nie występuje. Sama kolejka wyposa-

żona jest w dwie podstawowe metody push i pop, w połącze-

Listing 16.

Szczegóły implementacji

struct

ActiveObjectImpl

{

ActiveObjectImpl

()

:

commandQueue_

(

new

ActivationQueue

(

10

))

,

commandQueueNotEmptyOrSomethingElse_

(

new

boost

::

condition

)

,

commandQueueNotFull_

(

new

boost

::

condition

)

,

mutex_

(

new

boost

::

mutex

)

{}

void

setupIface

(

ActiveObject

*

iface

)

{

iface

->

doSomething

.

setSync

(

commandQueue_

,

mutex_

,

commandQueueNotEmptyOrSomethingElse_

,

commandQueueNotFull_

);

iface

->

doSomething

+=

delegate_cast

<

Event

>(

this

,

&

ActiveObjectImpl

::

onDoSomething

);

}

void

onDoSomething

(

int

x

);

void

onExecute

();

boost

::

shared_ptr

<

ActivationQueue

>

commandQueue_

;

boost

::

shared_ptr

<

boost

::

condition

>

commandQueueNotEmptyOrSomethingElse_

;

boost

::

shared_ptr

<

boost

::

condition

>

commandQueueNotFull_

;

boost

::

shared_ptr

<

boost

::

mutex

>

mutex_

;

boost

::

shared_ptr

<

boost

::

thread

>

thread_

;

volatile

bool

terminate_

;

}

;

Listing 17.

Inicjalizacja, uruchomienie i zatrzymanie

ActiveObject

ActiveObject

::

ActiveObject

()

:

impl_

(

new

ActiveObjectImpl

())

{

impl_

->

setupIface

(

this

);

}

void

ActiveObject

::

start

()

{

impl_

->

thread_

=

boost

::

shared_ptr

<

boost

::

thread

>(

new

boost

::

thread

(

delegate_cast

<

Delegate

>(

impl_

.

get

()

,

&

ActiveObjectImpl

::

onExecute

)));

}

void

ActiveObject

::

stop

()

{

impl_

->

terminate_

=

true

;

impl_

->

commandQueueNotEmptyOrSomethingElse_

->

notify_all

();

if

(

impl_

->

thread_

!=

0

)

impl_

->

thread_

->

join

();

}

Listing 18.

Głowna pętla Schedulera

void

ActiveObjectImpl

::

onExecute

()

{

terminate_

=

false

;

boost

::

mutex

::

scoped_lock

lock

(*

mutex_

);

while

(!

terminate_

)

{

if

(

PeekAndProcess

(

commandQueue_

.

get

()

,

lock

,

*

commandQueueNotFull_

)

)

continue

;

commandQueueNotEmptyOrSomethingElse_

->

wait

(

lock

);

}

}

;

background image

Programowanie aplikacji wielowątkowych w C++

67

www.sdjournal.org

Software Developer’s Journal 07/2007

niu z metodami alokacyjnymi daje komplet par funkcjonalno-

ści:

operator new + push

oraz

pop + operator delete

. Zarów-

no alokacja jak i pobranie kolejnego elementu z kolejki mogą

zwrócić pusty wskaźnik odpowiadający dwóm przypadkom:

gdy operator new zwróci pusty wskaźnik, oznacza to, że roz-

miar kolejki osiągnął rozmiar maksymalny i musimy poczekać

na zwolnienie jakiegoś elementu – występuje to tylko dla ko-

lejek z ograniczoną maksymalną ilością elementów, z drugiej

strony, gdy metoda pop zwróci pusty wskaźnik, oznacza to,

że kolejka jest pusta i musimy zaczekać na włożenie jakiegoś

elementu do tej kolejki.

Oba warunki wyznaczają użycie kolejki w kontekście wielo-

wątkowości. Potrzebujemy dwóch obiektów

Condition: notFull

– oznaczającego sytuację gdy kolejka nie jest „zapchana” oraz

notEmpty

– notyfikowanego gdy kolejka jest niepusta [Schm00].

Kolejka powinna być za to chroniona Sekcją Krytyczną realizo-

waną pojedynczym Muteksem. Scenariusz użycia kolejki mo-

żemy więc opisać w sposób opisany na Listingu 7.

Element Kolejki będzie bazową klasą dla wszystkich ele-

mentów, które w niej mogą być przechowywane – w tym przy-

padku będą to Komendy. Komenda sama w sobie powinna

pozwolić na swoją ewaluację. Aby to zapewnić, abstrakcyjna

komenda zostanie wyposażona w wirtualny operator wywoła-

nia funkcji, który musi być przeciążony w każdej Komendzie.

Ciało tego operatora będzie sercem komendy.

Komenda ewaluuje swoje argumenty, które muszą być do

niej podane z zewnątrz. Potrzebny jest więc generyczny me-

chanizm pozwalający na przechowanie listy argumentów. To

żądanie realizowane jest za pomocą szablonu

ArgList

.

Do ewaluacji listy argumentów w kontekście dowolnego

funktora stworzony zostanie generyczny szablon funkcji

Cal-

lWithArgs

.

Dysponując powyższymi narzędziami można przystąpić

do implementacji generycznej komendy, która będzie prze-

chowywana w Kolejce Aktywacji. Jako narzędzie użyty zosta-

nie również zbiór narzędzi opisany w artykule z kwietnia, doty-

czącego wzorca polimorfizmu zewnętrznego [Kapl07], do któ-

rego odsyłam czytelników.

Schemat pobrania Komendy z Kolejki Aktywacji, przetwo-

rzenia Komendy oraz zwolnienia Komendy realizuje funkcja

PeekAndProcess

.

Sama implementacja Proxy przedstawiona na Listingu

13. wymaga podania explicite podstawowych elementów syn-

chronizacyjnych oraz Kolejki Aktywacji używanej przez Proxy

w metodzie

setSync

. Schemat stworzenia Komendy, wypełnie-

nia jej listą argumentów oraz wstawienia jej do kolejki zaszyty

jest w implementacji wirtualnego operatora wywołania funkcji.

Dodatkowo operator opiekuje się odpowiednią obsługą podsta-

wowych elementów synchronizacyjnych (Mutex i Condition).

ActiveObject typu Hello-World

Otrzymane podstawowe mechanizmy, pomimo że wydają się

skomplikowane, pozwalają na bardzo prostą produkcję roz-

wiązań wielowątkowych. Rozpatrzmy przykładową implemen-

tację ActiveObjectu. W interfejsie publicznym (udostępnianym

Klientowi) użyjemy Proxy Komendy.

Całość implementacji naszego ActiveObjektu ukryjemy

przed Klientami poprzez realizację wzorca PIMPL (ang. Pri-
vate Implementation
). ActiveObject musi zostać zainicjalizo-

wany, uruchomiony oraz zatrzymany. Główny wątek Active-
Objectu
procesuje Kolejkę Aktywacji. Z punktu widzenia Klien-

ta ActiveObject interfejs analogiczny do zwykłego obiektu co

ukazuje piękno przyjętego rozwiązania.

Omówiono tu pokrótce elementy można pobrać w formie

biblioteki ze strony http://www.digital-advanced.com. Serdecz-

nie namawiam do eksperymentów. n

Listing 19.

Metoda wywoływana w kontekście wątka

ActiveObject

void

ActiveObjectImpl

::

onDoSomething

(

int

x

)

{

std

::

cout

<<

"hello:"

<<

x

<<

std

::

endl

;

}

Listing 20.

Wywołanie metody ActiveObject poprzez

Proxy

Singleton

_IMPL

(

ActiveObject

);

void

main

(

char

*[]

,

int

)

{

Singleton

<

ActiveObject

>

sr

;

sr

->

start

();

sr

->

doSomething

(

123

)

...

}

Bibliografia

• [Abel96] Herald Abelson, Gerald J.Sussman. Structure and In-

terpretation of Computer Programs. Struktura i Interpretacja
Programów Komputerowych. ISBN: 8320427126, Warszawa
2002, Wydawnictwo Naukowo-Techniczne;

• [Abra04] David Abrahams, Aleksey Gurtovoy. C++ Templa-

te Metaprogramming. ISBN: 0321227255: Addison-Wesley,
2004;

• [Alex01] Andrei Alexandrescu. Modern C++ Design: Ge-

neric Programming and Design Patterns Applied. ISBN:
0201704315, Boston, MA: Addison-Wesley, 2001;

• [Alex03] Andrei Alexandrescu. volatile – Multithreaded Pro-

grammer's Best Friend. Dr.Dobb’s. IV 2003. http://www.
ddj.com/dept/cpp/184403766
;

• [Gamm95] E. Gamma, R. Helm, R. Johnson, and J. Vlissides.

Design Patterns: Elements of Reusable Object-Oriented So-
ftware; Reading, MA: Addison-Wesley, 1995;

• [Kapl07] Pawel Kaplanski. „Ewolucja wzorca polimorfizmu ze-

wnętrznego w języku C++”. Software Developer’s Jurnal – IV
2007;

• [Lipp96] Stanley B. Lippman. Inside the C++ Object Model.

ISBN: 0201834545: Addison-Wesley, 1996;

• [Sanl99] T. Sanli, V. Kulkarini, Optimal Admission to Reader-

Writer Systems with no Queuing, Operations Research Let-
ters 25:213–218, 1999;

• [Schm00] Douglas Schmidt, Michael Stal, Hans Rohnert,

Frank Buschmann. Pattern-Oriented Software Architecture,
Volume 2, Patterns for Concurrent and Networked Objects
ISBN: 9780471606956: Addison-Wesley, 2000;

• [Schm96] Douglas C. Schmidt. Reality check. C++ Report,

March – http://www.cs.wustl.edu/~schmidt/editorial-3.html;

• [Schm98] Douglas C. Schmidt 1998, 1999. Scoped Locking –

http://www.cs.wustl.edu/~schmidt/PDF/ScopedLocking.pdf.


Wyszukiwarka

Podobne podstrony:
2007 07 Bezpieczne aplikacje Web w oparciu o ASP NET2 0
2007 05 Mechanizm koncepcji w języku C nowe oblicze szablonów [Inzynieria Oprogramowania]
2007 05 Mechanizm koncepcji w języku C nowe oblicze szablonów [Inzynieria Oprogramowania]
Wzorce projektowe Elementy oprogramowania obiektowego wielokrotnego uzytku wzoele
2007 07 trendy
Programowanie aplikacji na iPhone
Programowanie współbieżne i rozproszone w języku Java stpiczynski
R-06-07, Programowanie, ! HTML, HTML 4 - Vademecum
PROGRAMOWANIE APLIKACJI U.- WYKŁAD, PROG. APLIKACJI UŻYTKOWYCH- WYKŁAD 11
kodeks karny 2007.07.12 - komunikacja, ustawy
Programowanie mikrokontrolerow 8051 w jezyku C

więcej podobnych podstron