1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
|
Objects of i(mutex) classes are used to protect shared data.
Before using mutexes the tthi(mutex) header file must be included.
One of the key characteristics of multi-threaded programs is that threads may
share data. Functions running as separate threads have access to all global
data, and may also share the local data of their parent threads. However,
unless proper measures are taken, this may easily result in data corruption,
as illustrated by the following simulation of some steps that could be
encountered in a multi-threaded program:
verb(---------------------------------------------------------------------------
Time step: Thread 1: var Thread 2: description
---------------------------------------------------------------------------
0 5
1 starts T1 active
2 writes var T1 commences writing
3 stopped Context switch
4 starts T2 active
5 writes var T2 commences writing
6 10 assigns 10 T2 writes 10
7 stopped Context switch
8 assigns 12 T1 writes 12
9 12
----------------------------------------------------------------------------)
In this example, threads 1 and 2 share variable tt(var), initially having
the value 5. At step 1 thread 1 starts, and starts to write a value into
tt(var). However, it is interrupted by a context switch, and thread 2 is
started (step 4). Thread 2 em(also) wants to write a value into tt(var), and
succeeds until time step 7, when another context switch takes place. By now
tt(var) is 10. However, thread 1 was also in the process of writing a value
into tt(var), and it is given a chance to complete its work: it assigns 12
to tt(var) in time step 8. Once time step 9 is reached, thread 2 proceeds on
the (erroneous) assumption that tt(var) must be equal to 10. Clearly, from the
point of view of thread 2 its data have been corrupted.
In this case data corruption was caused by multiple threads accessing the same
data in an uncontrolled way. To prevent this from happening, access to shared
data should be protected in such a way that only one thread at a time may
access the shared data.
em(Mutexes) are used to prevent the abovementioned kinds of problems by
offering a guarantee that data are only accessed by the thread that could lock
the mutex that is used to synchronize access to those data.
Exclusive data access completely depends on cooperation between the
threads. If thread 1 uses mutexes, but thread 2 doesn't, then thread 2 may
freely access the common data. Of course that's bad practice, which should be
avoided.
It is stressed that although em(using) mutexes is the programmer's
responsibility, their em(implementation) isn't: mutexes offer the necessary
atomic calls. When requesting a mutex-lock the thread is blocked (i.e., the
mutex statement does not return) until the lock has been obtained by the
requesting thread.
Apart from the class tt(std::mutex) the class hi(recursive_mutex)
tt(std::recursive_mutex) is available. When a tt(recursive_mutex) is called
multiple times by the same thread it increases its lock-count. Before other
threads may access the protected data the recursive mutex must be unlocked
again that number of times. Moreover, the classes
hi(timed_mutex)tt(std::timed_mutex)
and
hi(recursive_timed_mutex)tt(std::recursive_timed_mutex)
are available. Their locks expire when released, but also after a certain
amount of time.
The members of the mutex classes perform emi(atomic actions): no context
switch occurs while they are active. So when two threads are trying to
em(lock) a mutex only one can succeed. In the above example: if both threads
would use a mutex to control access to tt(var) thread 2 would not have been
able to assign 12 to tt(var), with thread 1 assuming that its value was 10. We
could even have two threads running purely parallel (e.g., on two separate
cores). E.g.:
verb(-------------------------------------------------------------------------
Time step: Thread 1: Thread 2: escription
-------------------------------------------------------------------------
1 starts starts T1 and T2 active
2 locks locks Both threads try to
lock the mutex
3 blocks... obtains lock T2 obtains the lock,
and T1 must wait
4 (blocked) processes var T2 processes var,
T1 still blocked
5 obtains lock releases lock T2 releases the lock,
and T1 immediately
obtains the lock
6 processes var now T1 processes var
7 releases lock T1 also releases the lock
-------------------------------------------------------------------------)
Although mutexes can directly be used in programs, this rarely happens. It is
more common to embed mutex handling in locking classes that make sure that the
mutex is automatically unlocked again when the mutex lock is no longer
needed. Therefore, this section merely offers an overview of the interfaces of
the mutex classes. Examples of their use will be given in the upcoming
sections (e.g., section ref(LOCKS)).
All mutex classes offer the following constructors and members:
itemization(
ittq(mutex() constexpr)
(The default tt(constexpr) constructor is the only available
constructor;)
ittq(~mutex())
(The destructor does em(not) unlock a locked mutex. If locked it must
explicitly be unlocked using the mutex's tt(unlock) member;)
ithtq(lock)(void lock())
(The calling thread blocks until it owns the mutex. Unless tt(lock) is
called for a recursive mutex a em(system_error) is thrown if the thread
already owns the lock. Recursive mutexes increment their internal
emi(lock count);)
ithtq(try_lock)(bool try_lock() noexcept)
(The calling thread tries to obtain ownership of the mutex. If
ownership is obtained, tt(true) is returned, otherwise tt(false). If the
calling thread already owns the lock tt(true) is also returned, and in this
case a recursive mutex also increments its internal emi(lock count);)
ithtq(unlock)(void unlock() noexcept)
(The calling thread releases ownership of the mutex. A
tt(system_error) is thrown if the thread does not own the
lock. A recursive mutex decrements its interal lock count, releasing
ownership of the mutex once the lock count has decayed to zero;)
)
The timed-mutex classes (tt(timed_mutex, recursive_timed_mutex)) also offer
these members:
itemization(
ithtq(try_lock_for)(bool try_lock_for(chrono::duration<Rep, Period> const
&relTime) noexcept)
(The calling thread tries to obtain ownership of the mutex within the
specified time interval. If ownership is obtained, tt(true) is returned,
otherwise tt(false). If the calling thread already owns the lock tt(true) is
also returned, and in this case a recursive timed mutex also increments its
internal emi(lock count). The tt(Rep) and tt(Duration) types are inferred from
the actual tt(relTime) argument. E.g.,
verb(std::timed_mutex timedMutex;
timedMutex.try_lock_for(chrono::seconds(5));)
)
ithtq(try_lock_until)(bool try_lock_until(chrono::time_point<Clock,
Duration> const &absTime) noexcept)
(The calling thread tries to obtain ownership of the mutex until
tt(absTime) has passed. If ownership is obtained, tt(true) is returned,
otherwise tt(false). If the calling thread already owns the lock tt(true) is
also returned, and in this case a recursive timed mutex also increments its
internal emi(lock count). The tt(Clock) and tt(Duration) types are inferred
from the actual tt(absTime) argument. E.g.,
verb(std::timed_mutex timedMutex;
timedMutex.try_lock_until(chrono::system_clock::now() + chrono::seconds(5));)
)
)
|