Académique Documents
Professionnel Documents
Culture Documents
, : 4 , : ,
$ : , $ : ,
, : , $ : 4
$ : 4 , : 4
$ : 4 , : 4
, : , $ : 4
,o the results are nondeterministic ' you may get different results when you
run the program more than once. ,o, it can be very difficult to reproduce
bugs. #ondeterministic execution is one of the things that ma$es writing
parallel programs much more difficult than writing serial programs.
%hances are, the programmer is not happy with all of the possible results
listed above. &robably wanted the value of a to be : after both threads finish.
(o achieve this, must ma$e the increment operation atomic. (hat is, must
prevent the interleaving of the instructions in a way that would interfere with
the additions.
- 9 -
%oncept of atomic operation. 1n atomic operation is one that executes
without any interference from other operations ' in other words, it executes
as one unit. (ypically build complex atomic operations up out of se5uences of
primitive operations. In our case the primitive operations are the individual
machine instructions.
More formally, if several atomic operations execute, the final result is
guaranteed to be the same as if the operations executed in some serial order.
In our case above, build an increment operation up out of loads, stores and
add machine instructions. Want the increment operation to be atomic.
2se synchroni"ation operations to ma$e code se5uences atomic. 0irst
synchroni"ation abstraction semaphores. 1 semaphore is, conceptually, a
counter that supports two atomic operations, & and ?. Here is the ,emaphore
interface from #achos
class 5emaphore {
public:
5emaphore(char* debugName int initial6alue);
~5emaphore();
void 7();
void 6();
"
Here is what the operations do
o ,emphore.name, count/ creates a semaphore and initiali"es the
counter to count.
o &./ 1tomically waits until the counter is greater than 8, then
decrements the counter and returns.
o ?./ 1tomically increments the counter.
Here is how we can use the semaphore to ma$e the sum example wor$
int a # $;
5emaphore *s;
void sum(int p) {
int t;
s*+7();
a%%;
t # a;
s*+6();
printf(&'d : a # 'd(n& p t);
"
void main() {
Thread *t # ne) Thread(&child&);
s # ne) 5emaphore(&s& ,);
t*+Fork(sum ,);
sum($);
"
We are using semaphores here to implement a mutual exclusion mechanism.
(he idea behind mutual exclusion is that only one thread at a time should be
- 10 -
allowed to do something. In this case, only one thread should access a. 2se
mutual exclusion to ma$e operations atomic. (he code that performs the
atomic operation is called a critical section.
,emaphores do much more than mutual exclusion. (hey can also be used to
synchroni"e producer)consumer programs. (he idea is that the producer is
generating data and the consumer is consuming data. ,o a 2nix pipe has a
producer and a consumer. @ou can also thin$ of a person typing at a $eyboard
as a producer and the shell program reading the characters as a consumer.
Here is the synchroni"ation problem ma$e sure that the consumer does not
get ahead of the producer. -ut, we would li$e the producer to be able to
produce without waiting for the consumer to consume. %an use semaphores
to do this. Here is how it wor$s
5emaphore *s;
void consumer(int dumm8) {
)hile (,) {
s*+7();
consume the ne9t unit of data
"
"
void producer(int dumm8) {
)hile (,) {
produce the ne9t unit of data
s*+6();
"
"
void main() {
s # ne) 5emaphore(&s& $);
Thread *t # ne) Thread(&consumer&);
t*+Fork(consumer ,);
t # ne) Thread(&producer&);
t*+Fork(producer ,);
"
In some sense the semaphore is an abstraction of the collection of data.
In the real world, pragmatics intrude. If we let the producer run forever and
never run the consumer, we have to store all of the produced data
somewhere. -ut no machine has an infinite amount of storage. ,o, we want
to let the producer to get ahead of the consumer if it can, but only a given
amount ahead. We need to implement a bounded buffer which can hold only
# items. If the bounded buffer is full, the producer must wait before it can put
any more data in.
5emaphore *full;
5emaphore *empt8;
void consumer(int dumm8) {
- 11 -
)hile (,) {
full*+7();
consume the ne9t unit of data
empt8*+6();
"
"
void producer(int dumm8) {
)hile (,) {
empt8*+7();
produce the ne9t unit of data
full*+6();
"
"
void main() {
empt8 # ne) 5emaphore(&empt8& N);
full # ne) 5emaphore(&full& $);
Thread *t # ne) Thread(&consumer&);
t*+Fork(consumer ,);
t # ne) Thread(&producer&);
t*+Fork(producer ,);
"
1n example of where you might use a producer and consumer in an operating
system is the console .a device that reads and writes characters from and to
the system console/. @ou would probably use semaphores to ma$e sure you
don3t try to read a character before it is typed.
,emaphores are one synchroni"ation abstraction. (here is another called
loc$s and condition variables.
Boc$s are an abstraction specifically for mutual exclusion only. Here is the
#achos loc$ interface
class 2ock {
public:
2ock(char* debugName); :: initiali;e lock to be
F<==
~2ock(); :: deallocate lock
void >c?uire(); :: these are the onl8 operations on a lock
void <elease(); :: the8 are both *atomic*
"
1 loc$ can be in one of two states loc$ed and unloc$ed. ,emantics of loc$
operations
o Boc$.name/ creates a loc$ that starts out in the unloc$ed state.
o 1c5uire./ 1tomically waits until the loc$ state is unloc$ed, then sets
the loc$ state to loc$ed.
o Release./ 1tomically changes the loc$ state to unloc$ed from loc$ed.
- 12 -
In assignment C you will implement loc$s in #achos on top of semaphores.
What are re5uirements for a loc$ing implementation?
o *nly one thread can ac5uire loc$ at a time. .safety/
o If multiple threads try to ac5uire an unloc$ed loc$, one of the threads
will get it. .liveness/
o 1ll unloc$s complete in finite time. .liveness/
What are desirable properties for a loc$ing implementation?
o <fficiency ta$e up as little resources as possible.
o 0airness threads ac5uire loc$ in the order they as$ for it. 1re also
wea$er forms of fairness.
o ,imple to use.
When use loc$s, typically associate a loc$ with pieces of data that multiple
threads access. When one thread wants to access a piece of data, it first
ac5uires the loc$. It then performs the access, then unloc$s the loc$. ,o, the
loc$ allows threads to perform complicated atomic operations on each piece of
data.
%an you implement unbounded buffer only using loc$s? (here is a problem ' if
the consumer wants to consume a piece of data before the producer produces
the data, it must wait. -ut loc$s do not allow the consumer to wait until the
producer produces the data. ,o, consumer must loop until the data is ready.
(his is bad because it wastes %&2 resources.
(here is another synchroni"ation abstraction called condition variables +ust for
this $ind of situation. Here is the #achos interface
class @ondition {
public:
@ondition(char* debugName);
~@ondition();
void Aait(2ock *condition2ock);
void 5ignal(2ock *condition2ock);
void Broadcast(2ock *condition2ock);
"
,emantics of condition variable operations
o %ondition.name/ creates a condition variable.
o Wait.Boc$ Dl/ 1tomically releases the loc$ and waits. When Wait
returns the loc$ will have been reac5uired.
o ,ignal.Boc$ Dl/ <nables one of the waiting threads to run. When
,ignal returns the loc$ is still ac5uired.
o -roadcast.Boc$ Dl/ <nables all of the waiting threads to run. When
-roadcast returns the loc$ is still ac5uired.
1ll loc$s must be the same. In assignment C you will implement condition
variables in #achos on top of semaphores.
- 13 -
(ypically, you associate a loc$ and a condition variable with a data structure.
-efore the program performs an operation on the data structure, it ac5uires
the loc$. If it has to wait before it can perform the operation, it uses the
condition variable to wait for another operation to bring the data structure
into a state where it can perform the operation. In some cases you need more
than one condition variable.
Bet3s say that we want to implement an unbounded buffer using loc$s and
condition variables. In this case we have : consumers.
2ock *l;
@ondition *c;
int avail # $;
void consumer(int dumm8) {
)hile (,) {
l*+>c?uire();
if (avail ## $) {
c*+Aait(l);
"
consume the ne9t unit of data
avail**;
l*+<elease();
"
"
void producer(int dumm8) {
)hile (,) {
l*+>c?uire();
produce the ne9t unit of data
avail%%;
c*+5ignal(l);
l*+<elease();
"
"
void main() {
l # ne) 2ock(&l&);
c # ne) @ondition(&c&);
Thread *t # ne) Thread(&consumer&);
t*+Fork(consumer ,);
Thread *t # ne) Thread(&consumer&);
t*+Fork(consumer 4);
t # ne) Thread(&producer&);
t*+Fork(producer ,);
"
(here are two variants of condition variables Hoare condition variables and
Mesa condition variables. 0or Hoare condition variables, when one thread
performs a 5ignal, the very next thread to run is the waiting thread. 0or
Mesa condition variables, there are no guarantees when the signalled thread
- 14 -
will run. *ther threads that ac5uire the loc$ can execute between the
signaller and the waiter. (he example above will wor$ with Hoare condition
variables but not with Mesa condition variables.
What is the problem with Mesa condition variables? %onsider the following
scenario (hree threads, thread C one producing data, threads : and E
consuming data.
o (hread : calls consumer, and suspends.
o (hread C calls producer, and signals thread :.
o Instead of thread : running next, thread E runs next, calls consumer,
and consumes the element. .#ote with Hoare monitors, thread :
would always run next, so this would not happen./
o (hread : runs, and tries to consume an item that is not there.
4epending on the data structure used to store produced items, may
get some $ind of illegal access error.
How can we fix this problem? Replace the if with a )hile.
void consumer(int dumm8) {
)hile (,) {
l*+>c?uire();
)hile (avail ## $) {
c*+Aait(l);
"
consume the ne9t unit of data
avail**;
l*+<elease();
"
"
In general, this is a crucial point. 1lways put )hile3s around your condition
variable code. If you don3t, you can get really obscure bugs that show up very
infre5uently.
In this example, what is the data that the loc$ and condition variable are
associated with? (he avail variable.
&eople have developed a programming abstraction that automatically
associates loc$s and condition variables with data. (his abstraction is called a
monitor. 1 monitor is a data structure plus a set of operations .sort of li$e an
abstract data type/. (he monitor also has a loc$ and, optionally, one or more
condition variables. ,ee notes for Becture C7.
(he compiler for the monitor language automatically inserts a loc$ operation
at the beginning of each routine and an unloc$ operation at the end of the
routine. ,o, programmer does not have to put in the loc$ operations.
Monitor languages were popular in the middle =83s ' they are in some sense
safer because they eliminate one possible programming error. -ut more
recent languages have tended not to support monitors explicitly, and expose
the loc$ing operations to the programmer. ,o the programmer has to insert
- 15 -
the loc$ and unloc$ operations by hand. ;ava ta$es a middle ground ' it
supports monitors, but also allows programmers to exert finer grain control
over the loc$ed sections by supporting synchroni"ed bloc$s within methods.
-ut synchroni"ed bloc$s still present a structured model of synchroni"ation,
so it is not possible to mismatch the loc$ ac5uire and release.
Baundromat <xample 1 local laudromat has switched to a computeri"ed
machine allocation scheme. (here are # machines, numbered C to #. -y the
front door there are & allocation stations. When you want to wash your
clothes, you go to an allocation station and put in your coins. (he allocation
station gives you a number, and you use that machine. (here are also &
deallocation stations. When your clothes finish, you give the number bac$ to
one of the deallocation stations, and someone else can use the machine. Here
is the alpha release of the machine allocation software
allocate(int dumm8) {
)hile (,) {
)ait for coins from user
n # get();
give number n to user
"
"
deallocate(int dumm8) {
)hile (,) {
)ait for number n from user
put(i);
"
"
main() {
for (i # $; i C 7; i%%) {
t # ne) Thread(&allocate&);
t*+Fork(allocate $);
t # ne) Thread(&deallocate&);
t*+Fork(deallocate $);
"
"
(he $ey parts of the scheduling are done in the two routines get and put,
which use an array data structure a to $eep trac$ of which machines are in
use and which are free.
int a-N.;
int get() {
for (i # $; i C N; i%%) {
if (a-i. ## $) {
a-i. # ,;
return(i%,);
"
"
- 16 -
"
void put(int i) {
a-i*,. # $;
"
It seems that the alpha software isn3t doing all that well. ;ust loo$ing at the
software, you can see that there are several synchroni"ation problems.
(he first problem is that sometimes two people are assigned to the same
machine. Why does this happen? We can fix this with a loc$
int a-N.;
2ock *l;
int get() {
l*+>c?uire();
for (i # $; i C N; i%%) {
if (a-i. ## $) {
a-i. # ,;
l*+<elease();
return(i%,);
"
"
l*+<elease();
"
void put(int i) {
l*+>c?uire();
a-i*,. # $;
l*+<elease();
"
,o now, have fixed the multiple assignment problem. -ut what happens if
someone comes in to the laundry when all of the machines are already ta$en?
What does the machine return? Must fix it so that the system waits until there
is a machine free before it returns a number. (he situation calls for condition
variables.
int a-N.;
2ock *l;
@ondition *c;
int get() {
l*+>c?uire();
)hile (,) {
for (i # $; i C N; i%%) {
if (a-i. ## $) {
a-i. # ,;
l*+<elease();
return(i%,);
"
- 17 -
"
c*+Aait(l);
"
"
void put(int i) {
l*+>c?uire();
a-i*,. # $;
c*+5ignal();
l*+<elease();
"
What data is the loc$ protecting? (he a array.
When would you use a broadcast operation? Whenever want to wa$e up all
waiting threads, not +ust one. 0or an event that happens only once. 0or
example, a bunch of threads may wait until a file is deleted. (he thread that
actually deleted the file could use a broadcast to wa$e up all of the threads.
1lso use a broadcast for allocation)deallocation of variable si"ed units.
<xample concurrent malloc)free.
2ock *l;
@ondition *c;
char *malloc(int s) {
l*+>c?uire();
)hile (cannot allocate a chunk of si;e s) {
c*+Aait(l);
"
allocate chunk of si;e s;
l*+<elease();
return pointer to allocated chunk
"
void free(char *m) {
l*+>c?uire();
deallocate m1
c*+Broadcast(l);
l*+<elease();
"
<xample with malloc)free. Initially start out with C8 bytes free.
(ime &rocess C &rocess : &rocess E
malloc.C8/ ' succeeds malloc.F/ ' suspends loc$ malloc.F/ suspends loc$
C gets loc$ ' waits
: gets loc$ ' waits
E free.C8/ ' broadcast
7 resume malloc.F/ ' succeeds
- 18 -
F resume malloc.F/ ' succeeds
6 malloc.G/ ' waits
G malloc.E/ ' waits
= free.F/ ' broadcast
H resume malloc.G/ ' waits
C8 resume malloc.E/ ' succeeds
What would happen if changed c*+Broadcast(l) to c*+5ignal(l)? 1t step
C8, process E would not wa$e up, and it would not get the chance to allocate
available memory. What would happen if changed )hile loop to an if?
@ou will be as$ed to implement condition variables as part of assignment C.
(he following implementation is I#%*RR<%(. &lease do not turn this
implementation in.
class @ondition {
private:
int )aiting;
5emaphore *sema;
"
void @ondition::Aait(2ock* l)
{
)aiting%%;
l*+<elease();
sema*+7();
l*+>c?uire();
"
void @ondition::5ignal(2ock* l)
{
if ()aiting + $) {
sema*+6();
)aiting**;
"
"
Why is this solution incorrect? -ecause in some cases the signalling thread
may wa$e up a waiting thread that called Wait after the signalling thread
called ,ignal.
- 19 -