1
Consider a classic producer-consumer case: the producer generates items to be
2
consumed by a consumer. The producer can only produce so many items before its
3
storage capacity has filled up and the client cannot consume more items than
4
the producer has produced.
6
At some point the producer will therefore have to wait until the client has
7
consumed enough so there's again space available in the producer's
8
storage. Similarly, the client will have to wait until the producer has
9
produced at least some items.
11
Locking and polling the amount of available items/storage at fixed time
12
intervals usually isn't a good option as it is in essence a wasteful scheme:
13
threads continue to wait during the full interval even though the condition to
14
continue may already have been met; reducing the interval, on the other hand,
15
isn't an attractive option either as it results in a relative increase of the
16
overhead associated with handling the associated mutexes.
18
Condition variables hi(condition_variable) may be used to solve these
19
kinds of problems. A thread simply sleeps until it is notified by another
20
thread. Generally this may be accomplished as follows:
23
- produce the next item
24
- wait until there's room to store the item,
25
then reduce the available room
27
- increment the number of items in store
30
- wait until there's an item in store,
31
then reduce the number of items in store
32
- remove the item from the store
33
- increment the number of available storage locations
34
- do something with the retrieved item
37
It is important that the two storage administrative tasks (registering the
38
number of available items and available storage locations) are either
39
performed by the client or by the producer. `Waiting' in this case means:
41
it() Get a lock on the variable containing the actual count
42
it() As long as the count is zero: wait, release the lock until
43
another thread has increased the count, re-acquire the lock.
45
it() Release the lock.
47
To implement this scheme a ti(condition_variable) is used. The variable
48
containing the actual count is called a ti(semaphore) and it can be protected
49
using a tt(mutex sem_mutex). In addition a tt(condition_variable condition) is
50
defined. The waiting process is defined in the following function tt(down)
51
implemented as follows:
55
unique_lock<mutex> lock(sem_mutex); // get the lock
56
while (semaphore == 0)
57
condition.wait(lock); // see 1, below.
58
--semaphore; // dec. semaphore count
59
} // the lock is released
61
At 1 the condition variable's tt(wait) member internally releases the
62
lock, wait for a notification to continue, and re-acquires the lock just
63
before returning. Consequently, tt(down)'s code always has complete and
64
unique control over tt(semaphore).
66
What about notifying the condition variable? This is handled by the
67
`increment the number ...' parts in the abovementioned produced and consumer
68
loops. These parts are defined by the following tt(up) function:
72
lock_guard<std::mutex> lock(sem_mutex); // get the lock
74
condition.notify_one(); // see 2, below
75
} // the lock is released
77
At 2 tt(semaphore) is always incremented. However, by using a postfix
78
increment it can be tested for being zero at the same time and if it was zero
79
initially then tt(semaphore) is now one. Consequently, the thread waiting for
80
tt(semaphore) being unequal to zero may now
81
continue. tt(Condition.notify_one)hi(notify_one) notifies a waiting thread
82
(see tt(down)'s implementation above). In situations where multiple threads
83
are waiting `ti(notify_all)' can be used.
85
Handling tt(semaphore) can very well be encapsulated in a class
86
tt(Semaphore), offering members tt(down) and tt(up). For a more extensive
87
discussion of semaphores see i(Tanenbaum, A.S.) (2006)
88
i(Structured Computer Organization), Pearson Prentice-Hall.
90
Using the semaphore facilities of the class tt(Semaphore) whose
91
constructor expects an initial value of its tt(semaphore) data member, the
92
classic producer and consumer case can now easily be implemented in the
93
following multi-threaded program+footnote(A more elaborate example of the
94
producer-consumer program is found in the tt(yo/stl/examples/events.cc) file
95
in the bf(C++) Annotations's source archive):
97
Semaphore available(10);
108
itemQueue.push(item);
117
size_t item = itemQueue.front();
120
process(item); // not implemented here
126
thread produce(producer);
127
thread consume(consumer);
136
You still need to check that the data is ready though, since condition
137
variables can suffer from what are called spurious wakes: The call to wait()
138
may return even though it wasn't notified by another thread. If you're worried
139
about getting this wrong, you can pass that responsibility off to the standard
140
library too, if you tell it what you're waiting for with a predicate. The new
141
C++11 lambda facility makes this really easy:
144
std::unique_lock<std::mutex> lk(m);
145
cond.wait(lk,[]{return data_ready;});
150
To use tt(condition_variable) objects the header file
151
ti(condition_variable)hi(#include <condition_variable>) must be included.
1
In this section em(condition variables) are introduced, allowing programs to
2
synchronize threads on the em(states) of data, rather than on the em(access)
3
to data, which is realized using mutexes.
5
Before using condition variables the tthi(condition_variable) header file must
8
To start our discussion, we consider a classic producer-consumer scenario: the
9
producer generates items to be consumed by a consumer. The producer can only
10
produce a certain number of items before its storage capacity has filled up
11
and the client cannot consume more items than the producer has produced.
13
At some point the producer has to wait until the client has consumed enough,
14
thus creating space in the producer's storage. Similarly, the consumer cannot
15
start consuming until the producer has at least produced some items.
17
Mutexes (data locking) don't result in elegant solutions of producer-consumer
18
types of problems, as using mutexes requires repeated locking and polling the
19
amount of available items/storage. This isn't a very attractive option as it
20
wastes resources. Polling forces threads to wait until they own the mutex,
21
even though continuation might already be possible. The polling interval could
22
be reduced, but that too isn't an attractive option, as it results in
23
needlessly increasing the overhead associated with handling the associated
26
On the other hand, condition variables allow you to avoid polling by
27
synchronizing threads using the em(states) (e.g., em(values)) of data.
29
As the the states of the data may be modified by multiple threads, threads
30
still have to use mutexes, but merely to control access to the data. However,
31
condition variables allow threads to release ownership of the mutex until a
32
certain state has been reached, until a preset amount of time has been passed,
33
or until a preset point in time has been reached.
35
The prototypical setup of these kinds of programs look like this:
37
it() consumer thread(s) act like this:
39
obtain ownership of the used mutex
40
while the required condition is false:
41
release the ownership and wait until being notified
42
continue processing now that the condition is true
43
release ownership of the mutex
45
it() producer thread(s) act like this:
47
obtain ownership of the used mutex
48
while the condition is false:
49
work towards changing the condition to true
50
signal other waiting threads that the condition is now true
51
release ownership of the mutex
55
Condition variables come in two flavors: objects of the class
56
hi(condition_variable)tt(std::condition_variable) are used in combination
57
with objects of type tt(unique_lock<mutex>). This combination allows
58
optimizations resulting in an increased efficiency compared to the efficiency
59
that can be obtained with objects of the class
60
hi(condition_variable_any)tt(stdLLcondition_variable_any) that can be used
61
with any (e.g., user-supplied) lock types.
63
The condition variable classes offer members like tt(wait, wait_for,
64
wait_until, notify_one) and tt(notify_all) that may concurrently be called.
65
The notify members are always atomically executed. Execution of the
66
tt(wait) members consists of three atomic parts:
68
it() the mutex's release, and subsequent entry into the waiting state;
69
it() unblocking the wait state;
70
it() reacquisition of the lock.
72
Therefore, returning from tt(wait)-members the thread calling wait owns
75
Programs using condition variables cannot make any assumption about the order
76
in which any of the tt(notify_one, notify_all, wait, wait_for), and
77
tt(wait_until) members are executed.
79
In addition to the condition variable classes the following free function and
80
tt(enum) type are provided:
82
itht(notify_all_at_thread_exit)(void
83
std::notify_all_at_thread_exit+OPENPARcondition_variable &cond,)
84
linebreak()tt(unique_lock<mutex> lockObject+CLOSEPAR:)
85
quote(once the current thread has ended, all other threads waiting on
86
tt(cond) will be notified. It is good practice to exit the thread as
87
soon as possible after calling linebreak()
88
tt(notify_all_at_thread_exit).
90
Waiting threads must verify that the thread they were waiting for has
91
indeed ended. This is usually implemented by obtaining the lock on
92
tt(lockObject), after which these threads verify that the condition
93
they were waiting for is true, and that the lock was not released and
94
reacquired before tt(notify_all_at_thread_exit) was called.)
97
The tt(cv_status) enum is used by several member functions of the
98
condition variable classes covered below: