WIP on stl's condition_variable sections

This commit is contained in:
Frank B. Brokken 2014-06-02 13:48:48 +02:00
parent dad3acc040
commit f83d210530
4 changed files with 220 additions and 133 deletions

View file

@ -120,7 +120,7 @@ includefile(stl/threading)
subsect(Synchronization (mutexes))
includefile(stl/mutex)
subsect(Locks and lock handling)
lsubsect(LOCKS)(Locks and lock handling)
includefile(stl/locks)
subsubsect(Deadlocks)
@ -129,10 +129,10 @@ includefile(stl/threading)
subsect(Event handling (condition variables))
includefile(stl/events)
subsubsect(The class 'condition_variable')
lsubsubsect(CONDVAR1)(The class 'condition_variable')
includefile(stl/conditionvar)
subsubsect(The class 'condition_variable_any')
lsubsubsect(CONDVAR2)(The class 'condition_variable_any')
includefile(stl/conditionany)
lsubsubsect(CONDEX)(An example using condition variables)

View file

@ -1,6 +1,7 @@
Different from the class tt(condition_variable) the class
ti(condition_variable_any) can be used with any (e.g., user-supplied) lock
type, and not just with the stl-provided tt(unique_lock<mutex>).
hi(condition_variable_any)tt(std::condition_variable_any) can be used with
any (e.g., user supplied) lock type, and not just with the stl-provided
tt(unique_lock<mutex>).
Before using the class tt(condition_variable_any) the tthi(condition_variable)
header file must have been included.
@ -8,8 +9,8 @@ header file must have been included.
The functionality that is offered by tt(condition_variable_any) is identical
to the functionality offered by the class tt(condition_variable), albeit that
the lock-type that is used by tt(condition_variable_any) is not
predefined. The class tt(condition_variable_any) requires specification of the
lock-type that must be used by its objects.
predefined. The class tt(condition_variable_any) therefore requires the
specification of the lock-type that must be used by its objects.
In the interface shown below this lock-type is referred to as ti(Lock). Most
of tt(condition_variable_any's) members are defined as member templates,
@ -25,7 +26,7 @@ instead of just tt(unique_lock) to corresponding members), the reader is
referred to the previous section for a description of the semantics of the
class members.
Like tt(condition_variable), the class tt(condition_variable_any) merely
Like tt(condition_variable), the class tt(condition_variable_any) only
offers a default constructor. No copy constructor or overloaded assignment
operator are provided.
@ -38,8 +39,19 @@ Note that, in addition to tt(Lock), the types tt(Clock, Duration, Period,
Predicate,) and tt(Rep) are template types, defined just like the identically
named types mentioned in the previous section.
The class tt(condition_variable_any's) members are:
Assuming that tt(MyMutex) is a user defined mutex type, and that tt(MyLock) is
a user defined lock-type (cf. section ref(LOCKS) for details about
lock-types), then a tt(condition_variable_any) object can be defined and used
like this:
verb(
MyMutex mut;
MyLock<MyMutex> ul(mut);
condition_variable_any cva;
cva.wait(ul);
)
Here are the class tt(condition_variable_any's) members:
itemization(
itht(notify_one)(void notify_one() noexcept;)
itht(notify_all)(void notify_all() noexcept;)
@ -58,3 +70,5 @@ The class tt(condition_variable_any's) members are:

View file

@ -1,93 +1,147 @@
The class ti(condition_variable) merely offers a default constructor. No copy
constructor or overloaded assignment operator are provided.
The class tt(std::condition_variable)hi(condition_variable) merely offers a
default constructor. No copy constructor or overloaded assignment operator are
provided.
Before using the class tt(condition_variable) the tthi(condition_variable)
header file must have been included.
The class's destructor requires that no thread is blocked by the current
thread. This implies that all other (waiting) threads must have been notified;
those threads may, however, subsequently block on the lock specified in their
tt(wait) calls.
thread. This implies that all threads waiting on a tt(condition_variable) must
have been notified before a tt(condition_variable) object's lifetime
ends. Calling tt(notify_all) (see below) before a tt(condition_variable)
ceases to exists takes care of that.
In the following member-descriptions a type tt(Predicate) indicates that the
provided tt(Predicate) argument can be called as a function without arguments,
returning a tt(bool). Also, other member functions are frequently referred
to. It is tacitly assumed that all members were called using the same
condition variable object.
to. It is tacitly assumed that all member referred to below were called using
the same condition variable object.
The class tt(condition_variable) supports several tt(wait) members, which will
block the thread until notified by another thread (or after a configurabel
waiting time). However, these tt(wait) members may also spuriously unblock,
without having reacquired the lock. Therefore, returning from these tt(wait)
members threads should verify that the required data condition has actually
been met. If not, again calling tt(wait) may be appropriate, as illustrated by
the next piece of pseudo code:
verb(
while (conditionNotYetMet())
condVariable.wait(&uniqueLock);
)
)
The class tt(condition_variable)'s members are:
itemization(
ithtq(notify_one)(void notify_one() noexcept)
(one tt(wait) member called by other threads returns. Which one
actually returns cannot be predicted.)
ithtq(notify_all)(void notify_all() noexcept)
(all tt(wait) members called by other threads unblock their wait
states. Of course, only one of them will subsequently succeed in
reacquiring the condition variable's lock object.)
ithtq(wait)(void wait(unique_lock<mutex>& lockObject))
(the current thread is blocked until it (usually) has obtained the lock
of tt(lockObject). However, tt(wait) may also spuriously unblock,
without having locked tt(lockObject). Therefore, returning from
tt(wait) threads should always verify that they have obtained the
lock. If not, again calling tt(wait) may be appropriate.)
ittq(void wait(unique_lock<mutex>& lock, Predicate pred))
(This is a member template, defining the template header tt(template
<typename Predicate>). As long as `tt(pred())' returns tt(false)
tt(wait(lock)) is called.)
ithtq(wait_for)(cv_status wait_for(unique_lock<mutex> &lockObject,
chrono::duration<Rep, Period> const &relTime))
(This member is defined as a member template, using the template header
tt(template <typename Rep, typename Period>). The tt(Rep) and
tt(Period) types are derived from the actual tt(relTime) argument
that is passed to this member, and should not explicitly be specified.
The tt(lockObject) must be locked by the current thread and either no
other thread is waiting on this tt(condition_variable) object, or
tt(lock.mutex()) returns the same value for each of the tt(lockObject)
arguments supplied by all currently waiting threads.
ithtq(wait)(void wait(unique_lock<mutex>& uniqueLock))
(before calling tt(wait) the current thread must have acquired the lock
of tt(uniqueLock). Calling tt(wait) releases the lock, and the current
thread is blocked until it has received a notification from another
thread, and has reacquired the lock.
This member calls tt(lockObject.unlock) and the current thread is
blocked. It unblocks when receiving a signal through a tt(notify)
member, when an interval specified by tt(relTime) has passed, or
spuriously. Once it unblocks it tries to reacquire the lock
on tt(lockObject). Before this member returns the current thread
has acquired the lock on tt(lockObject). If returning due to a
timeout, tt(cv_status::timeout) is returned, otherwise
tt(cv_status::no_timeout) is returned.)
ittq(bool wait_for(unique_lock<mutex> &lockObject,
Threads should verify that the required data condition has been met
after tt(wait) has returned.)
ittq(void wait(unique_lock<mutex>& uniqueLock, Predicate pred))
(this is a member template, using the template header tt(template
<typename Predicate>).
The template's type is automatically derived from the function's
argument type and does not have to be specified explicitly.
Before calling tt(wait) the current thread must have acquired the lock
of tt(uniqueLock). As long as `tt(pred)' returns tt(false)
tt(wait(lock)) is called.
Threads should verify that the required data condition has been met
after tt(wait) has returned.)
ithtq(wait_for)(cv_status wait_for(unique_lock<mutex> &uniqueLock,
std::chrono::duration<Rep, Period> const &relTime))
(this member is defined as a member template, using the template header
tt(template <typename Rep, typename Period>).
The template's types are automatically derived from the typs of the
function's arguments and do not have to be specified explicitly.
E.g., to wait for at most 5 seconds tt(wait_for) can be called like
this:
verb(
cond.wait_for(&unique_lock, std::chrono::seconds(5));
)
This member returns when being notified or when the time interval
specified by tt(relTime) has passed. When returning due to a timeout,
tt(std::chrono::cv_status::timeout) is returned, otherwise
tt(std::chrono::cv_status::no_timeout) is returned.
Threads should verify that the required data condition has been met
after tt(wait_for) has returned.)
ittq(bool wait_for(unique_lock<mutex> &uniqueLock,
chrono::duration<Rep, Period> const &relTime, Predicate
pred))
(this member is also defined as a member template, using the template
(this member is defined as a member template, using the template
header tt(template <typename Rep, typename Period, typename
Predicate>). The template types are automatically derived from the
types of the arguments that passed to this member.
Predicate>).
The template's types are automatically derived from the typs of the
function's arguments and do not have to be specified explicitly.
As long as tt(pred()) returns false, the previous member is called. If
the previous member returns tt(cv_status::timeout), then tt(pred()) is
returned, otherwise tt(true).)
ithtq(wait_until)(cv_status wait_until(unique_lock<mutex>& lockObject,
As long as tt(pred) returns false, the previous tt(wait_for) member is
called. If the previous member returns tt(cv_status::timeout), then
tt(pred) is returned, otherwise tt(true).
Threads should verify that the required data condition has been met
after tt(wait_for) has returned.)
ithtq(wait_until)(cv_status wait_until(unique_lock<mutex>& uniqueLock,
chrono::time_point<Clock, Duration> const &absTime))
(This member is also defined as a member template, using the template
header tt(template <typename Clock, typename Duration>). The template
types are derived from the types of the arguments that are passed to
this member and do not have to be specified explicitly.
(this member is defined as a member template, using the template
header tt(template <typename Clock, typename Duration>).
The template's types are automatically derived from the typs of the
function's arguments and do not have to be specified explicitly.
E.g., to wait until 5 minutes after the current time tt(wait_until) can
be called like this:
verb(
cond.wait_until(&unique_lock, chrono::system_clock::now() +
std::chrono::minutes(5));
)
This function acts identically to the tt(wait_for(unique_lock<mutex>
&lockObject, chrono::duration<Rep, Period> const &relTime)) member,
but uses an absolute point in time, rather than a relative time
specification. If returning due to a timeout, tt(cv_status::timeout)
is returned, otherwise tt(cv_status::no_timeout) is returned.)
&uniqueLock, chrono::duration<Rep, Period> const &relTime)) member
described earlier, but uses an absolute point in time, rather than a
relative time specification.
This member returns when being notified or when the time interval
specified by tt(relTime) has passed. When returning due to a timeout,
tt(std::chrono::cv_status::timeout) is returned, otherwise
tt(std::chrono::cv_status::no_timeout) is returned.
Threads should verify that the required data condition has been met
after tt(wait_until) has returned.)
ittq(bool wait_until(unique_lock<mutex> &lock,
chrono::time_point<Clock, Duration> const &absTime,
Predicate pred))
(this member is also defined as a member template, using the template
header tt(template <typename Clock, typename Duration, typename
Predicate>). The template types are derived from the types of the
arguments that are passed to this member and do not have to be
specified explicitly.
(this member is defined as a member template, using the template header
tt(template <typename Clock, typename Duration, typename Predicate>).
The template's types are automatically derived from the types of the
function's arguments and do not have to be specified explicitly.
As long as tt(pred()) returns false, the previous member is called. If
the previous member returns tt(cv_status::timeout), then tt(pred()) is
returned, otherwise tt(true). )
As long as tt(pred) returns false, the previous tt(wait_until) member
is called. If the previous member returns tt(cv_status::timeout), then
tt(pred) is returned, otherwise tt(true).
Threads should verify that the required data condition has been met
after tt(wait_until) has returned.)
)

View file

@ -1,101 +1,118 @@
In this section em(condition variables) are introduced, allowing programs to
synchronize threads on the em(states) of data, rather than on the em(access)
to data, which is realized using mutexes.
In this section em(condition variables), as defined by the standard template
library, are introduced. Condition variables allow programs to synchronize
threads using the em(states) of data, rather than simply locking the
em(access) to data (which is realized using mutexes).
Before using condition variables the tthi(condition_variable) header file must
have been included.
Before condition variables can be used the tthi(condition_variable) header
file must have been included.
To start our discussion, we consider a classic producer-consumer scenario: the
producer generates items to be consumed by a consumer. The producer can only
produce a certain number of items before its storage capacity has filled up
and the client cannot consume more items than the producer has produced.
To start our discussion, consider a classic producer-consumer scenario: the
producer generates items which are consumed by a consumer. The producer can
only produce a certain number of items before its storage capacity has filled
up and the client cannot consume more items than the producer has produced.
At some point the producer has to wait until the client has consumed enough,
thus creating space in the producer's storage. Similarly, the consumer cannot
start consuming until the producer has at least produced some items.
At some point the producer's storage capacity has filled to the brim, and the
producer has to wait until the client has at least consumed some items,
thereby creating space in the producer's storage. Similarly, the consumer
cannot start consuming until the producer has at least produced some items.
Mutexes (data locking) don't result in elegant solutions of producer-consumer
types of problems, as using mutexes requires repeated locking and polling the
amount of available items/storage. This isn't a very attractive option as it
wastes resources. Polling forces threads to wait until they own the mutex,
even though continuation might already be possible. The polling interval could
be reduced, but that too isn't an attractive option, as it results in
needlessly increasing the overhead associated with handling the associated
mutexes.
Implementing this scenario only using mutexes (data locking) is not an
attractive option, as merely using mutexes forces a program to implement the
scenario using em(polling): processes must continuously (re)acquire the
mutex's lock, determine whether they can perform some action, followed by the
release of the lock. Often there's no action to perform, and the process is
busy acquiring and releasing the mutex's lock. Polling forces threads to wait
until they can lock the mutex, even though continuation might already be
possible. The polling interval could be reduced, but that too isn't an
attractive option, as it results in needlessly increasing the overhead
associated with handling the associated mutexes (a situation also known as
`busy waiting').
On the other hand, condition variables allow you to avoid polling by
synchronizing threads using the em(states) (e.g., em(values)) of data.
Contrary to merely using mutexes, polling can be prevented using condition
variables. Using condition variables threads may em(notify) waiting threads
that there is something for them to do. Thus threads synchronized on the
em(states) (e.g., em(values)) of data.
As the the states of the data may be modified by multiple threads, threads
still have to use mutexes, but merely to control access to the data. However,
condition variables allow threads to release ownership of the mutex until a
certain state has been reached, until a preset amount of time has been passed,
or until a preset point in time has been reached.
As the states of data may be modified by multiple threads, threads still need
to use mutexes, but merely to control access to the data. In addition,
however, condition variables allow threads to release ownership of mutexes
until a certain state has been reached, until a preset amount of time has been
passed, or until a preset point in time has been reached.
The prototypical setup of these kinds of programs look like this:
The prototypical setup of threads using condition variables looks like this:
itemization(
it() consumer thread(s) act like this:
verb(
obtain ownership of the used mutex
lock the mutex
while the required condition is false:
release the ownership and wait until being notified
continue processing now that the condition is true
release ownership of the mutex
wait until being notified
(automatically releasing the mutex's lock).
the mutex's lock has been reacquired, and the condition is true:
process the data
release the mutex's lock.
)
it() producer thread(s) act like this:
it() producer thread(s) act similarly:
verb(
obtain ownership of the used mutex
lock the mutex
while the condition is false:
work towards changing the condition to true
signal other waiting threads that the condition is now true
release ownership of the mutex
process the data
notify waiting threads that the condition is true
release the mutex's lock.
)
)
) No matter which thread starts, the thread holding the mutex's lock will
at some point release the lock, allowing the other process to (re)acquire
it. If the consumer starts it immediately releases the lock once it enters its
waiting state; if the producer starts it releases the lock once the condition
is true. There is a slight initial synchronization requirement, though. The
producer's notification will be missed if the consumer hasn't yet entered its
waiting state. So waiting (consumer) threads should start before notifying
(producer) threads. One the threads have started, no assumptions can be made
about the order in which any of the tt(notify_one, notify_all, wait,
wait_for), and tt(wait_until) members are executed.
Condition variables come in two flavors: objects of the class
hi(condition_variable)tt(std::condition_variable) are used in combination
with objects of type tt(unique_lock<mutex>). This combination allows
with objects of type tt(unique_lock<mutex>). This allows for certain
optimizations resulting in an increased efficiency compared to the efficiency
that can be obtained with objects of the class
hi(condition_variable_any)tt(std::condition_variable_any) that can be used
with any (e.g., user-supplied) lock type.
hi(condition_variable_any)tt(std::condition_variable_any), which may be
used with any (e.g., user supplied) lock type.
The condition variable classes offer members like tt(wait, wait_for,
Condition variable classes offer members like tt(wait, wait_for,
wait_until, notify_one) and tt(notify_all) that may concurrently be called.
The notify members are always atomically executed. Execution of the
The notifying members are always atomically executed. Execution of the
tt(wait) members consists of three atomic parts:
itemization(
it() the mutex's release, and subsequent entry into the waiting state;
it() unblocking the wait state;
it() reacquisition of the lock.
it() the mutex is released, and the thread is suspended until its
notification;
it() Once the notification has been received, the lock is reacquired
it() The wait state ends (and processing continues beyond the tt(wait)
call).
)
Therefore, returning from tt(wait)-members the thread calling wait owns
the lock.
Programs using condition variables cannot make any assumption about the order
in which any of the tt(notify_one, notify_all, wait, wait_for), and
tt(wait_until) members are executed.
So, returning from tt(wait)-members the previously waiting thread
has reacquired the mutex's lock.
In addition to the condition variable classes the following free function and
tt(enum) type are provided:
tt(enum) type is provided:
itemization(
itht(notify_all_at_thread_exit)(void
ithtq(notify_all_at_thread_exit)(void
std::notify_all_at_thread_exit+OPENPARcondition_variable &cond,)
linebreak()tt(unique_lock<mutex> lockObject+CLOSEPAR:)
quote(once the current thread has ended, all other threads waiting on
tt(cond) will be notified. It is good practice to exit the thread as
linebreak()tt(unique_lock<mutex> lockObject+CLOSEPAR)
(once the current thread has ended, all other threads waiting on
tt(cond) are notified. It is good practice to exit the thread as
soon as possible after calling linebreak()
tt(notify_all_at_thread_exit).
Waiting threads must verify that the thread they were waiting for has
indeed ended. This is usually implemented by obtaining the lock on
tt(lockObject), after which these threads verify that the condition
they were waiting for is true, and that the lock was not released and
indeed ended. This is usually realized by first obtaining the lock on
tt(lockObject), followed by verifying that the condition
they were waiting for is true and that the lock was not
reacquired before tt(notify_all_at_thread_exit) was called.)
it() hi(cv_status)
quote(
ithtq(cv_status)(std::cv_status)
(
The tt(cv_status) enum is used by several member functions of the
condition variable classes covered below:
condition variable classes (cf. sections ref(CONDVAR1) and ref(CONDVAR2)):
verb(
namespace std
{
@ -106,5 +123,7 @@ namespace std
};
}
)
)
)
)