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.