Sie sind auf Seite 1von 6

Locks, Mutexes, and Semaphores: Types of

Synchronization Objects
Tuesday, 21 October 2014
I recently got an email asking about locks and different types of synchronization
objects, so I'm posting this entry in case it is of use to others.
Locks
A lock is an abstract concept. The basic premise is that a lock protects access to
some kind of shared resource. If you own a lock then you can access the protected
shared resource. If you do not own the lock then you cannot access the shared
resource.
To own a lock, you first need some kind of lockable object. You then acquire the
lock from that object. The precise terminology may vary. For example, if you have
a lockable object XYZ you may:

 acquire the lock on XYZ,


 take the lock on XYZ,
 lock XYZ,
 take ownership of XYZ,
 or some similar term specific to the type of XYZ

The concept of a lock also implies some kind of exclusion: sometimes you might be
unable to take ownership of a lock, and the operation to do so will either fail,
or block. In the former case, the operation will return some error code or exception
to indicate that the attempt to take ownership failed. In the latter case, the
operation will not return until it has taken ownership, which typically requires that
another thread in the system does something to permit that to happen.
The most common form of exclusion is a simple numerical count: the lockable
object has a maximum number of owners. If that number has been reached, then
any further attempt to acquire a lock on it will be unable to succeed. This therefore
requires that we have some mechanism of relinquishing ownership when we are
done. This is commonly called unlocking, but again the terminology may vary. For
example, you may:

 release the lock on XYZ,


 drop the lock on XYZ,
 unlock XYZ,
 relinquish ownership of XYZ,
 or some similar term specific to the type of XYZ
When you relinquish ownership in the appropriate fashion then a blocked operation
that is trying to acquire the lock may not proceed, if the required conditions have
been met.
For example if a lockable object only allows 3 owners then a 4th attempt to acquire
the lock will block. When one of the first 3 owners releases the lock then that 4th
attempt to acquire the lock will succeed.
Ownership
What it means to "own" a lock depends on the precise type of the lockable object.
For some lockable objects there is a very tight definition of ownership: this specific
thread owns the lock, through the use of that specific object, within this particular
scope.
In other cases, the definition is more fluid, and the ownership of the lock is more
conceptual. In these cases, ownership can be relinquished by a different thread or
object than the thread or object that acquired the lock.
Mutexes
Mutex is short for MUTual EXclusion. Unless the word is qualified with additional
terms such as shared mutex, recursive mutex or read/write mutex then it refers to
a type of lockable object that can be owned by exactly one thread at a time. Only
the thread that acquired the lock can release the lock on a mutex. When the mutex
is locked, any attempt to acquire the lock will fail or block, even if that attempt is
done by the same thread.
Recursive Mutexes
A recursive mutex is similar to a plain mutex, but one thread may own multiple
locks on it at the same time. If a lock on a recursive mutex has been acquired by
thread A, then thread A can acquire further locks on the recursive mutex without
releasing the locks already held. However, thread B cannot acquire any locks on the
recursive mutex until allthe locks held by thread A have been released.
In most cases, a recursive mutex is undesirable, since the it makes it harder to
reason correctly about the code. With a plain mutex, if you ensure that the
invariants on the protected resource are valid before you release ownership then
you know that when you acquire ownership those invariants will be valid.
With a recursive mutex this is not the case, since being able to acquire the lock
does not mean that the lock was not already held, by the current thread, and
therefore does not imply that the invariants are valid.
Reader/Writer Mutexes
Sometimes called shared mutexes, multiple-reader/single-writer mutexes or
just read/write mutexes, these offer two distinct types of ownership:

 shared ownership, also called read ownership, or a read lock, and


 exclusive ownership, also called write ownership, or a write lock.

Exclusive ownership works just like ownership of a plain mutex: only one thread
may hold an exclusive lock on the mutex, only that thread can release the lock. No
other thread may hold any type of lock on the mutex whilst that thread holds its
lock.
Shared ownership is more lax. Any number of threads may take shared ownership
of a mutex at the same time. No thread may take an exclusive lock on the mutex
while any thread holds a shared lock.
These mutexes are typically used for protecting shared data that is seldom
updated, but cannot be safely updated if any thread is reading it. The reading
threads thus take shared ownership while they are reading the data. When the data
needs to be modified, the modifying thread first takes exclusive ownership of the
mutex, thus ensuring that no other thread is reading it, then releases the exclusive
lock after the modification has been done.
Spinlocks
A spinlock is a special type of mutex that does not use OS synchronization functions
when a lock operation has to wait. Instead, it just keeps trying to update the mutex
data structure to take the lock in a loop.
If the lock is not held very often, and/or is only held for very short periods, then
this can be more efficient than calling heavyweight thread synchronization
functions. However, if the processor has to loop too many times then it is just
wasting time doing nothing, and the system would do better if the OS scheduled
another thread with active work to do instead of the thread failing to acquire the
spinlock.
Semaphores
A semaphore is a very relaxed type of lockable object. A given semaphore has a
predefined maximum count, and a current count. You take ownership of a
semaphore with a wait operation, also referred to as decrementing the semaphore,
or even just abstractly called P. You release ownership with a signal operation, also
referred to as incrementing the semaphore, a post operation, or abstractly called V.
The single-letter operation names are from Dijkstra's original paper on semaphores.
Every time you wait on a semaphore, you decrease the current count. If the count
was greater than zero then the decrement just happens, and the wait call returns.
If the count was already zero then it cannot be decremented, so the wait call
will block until another thread increases the count by signalling the semaphore.
Every time you signal a semaphore, you increase the current count. If the count
was zero before you called signal, and there was a thread blocked in wait then that
thread will be woken. If multiple threads were waiting, only one will be woken. If
the count was already at its maximum value then the signal is typically ignored,
although some semaphores may report an error.
Whereas mutex ownership is tied very tightly to a thread, and only the thread that
acquired the lock on a mutex can release it, semaphore ownership is far more
relaxed and ephemeral. Any thread can signal a semaphore, at any time, whether
or not that thread has previously waited for the semaphore.
An analogy
A semaphore is like a public lending library with no late fees. They might have 5
copies of C++ Concurrency in Actionavailable to borrow. The first five people that
come to the library looking for a copy will get one, but the sixth person will either
have to wait, or go away and come back later.
The library doesn't care who returns the books, since there are no late fees, but
when they do get a copy returned, then it will be given to one of the people waiting
for it. If no-one is waiting, the book will go on the shelf until someone does want a
copy.
Binary semaphores and Mutexes
A binary semaphore is a semaphore with a maximum count of 1. You can use a
binary semaphore as a mutex by requiring that a thread only signals the
semaphore (to unlock the mutex) if it was the thread that last successfully waited
on it (when it locked the mutex). However, this is only a convention; the
semaphore itself doesn't care, and won't complain if the "wrong" thread signals the
semaphore.
Critical Sections
In synchronization terms, a critical section is that block of code during which a lock
is owned. It starts at the point that the lock is acquired, and ends at the point that
the lock is released.
Windows CRITICAL_SECTIONs

Windows programmers may well be familiar with CRITICAL_SECTION objects.


A CRITICAL_SECTION is a specific type of mutex, not a use of the general
term critical section.
Mutexes in C++
The C++14 standard has five mutex types:

 std::mutex
 std::timed_mutex
 std::recursive_mutex
 std::recursive_timed_mutex
 std::shared_timed_mutex
The variants with "timed" in the name are the same as those without, except that
the lock operations can have time-outs specified, to limit the maximum wait time. If
no time-out is specified (or possible) then the lock operations will block until the
lock can be acquired — potentially forever if the thread that holds the lock never
releases it.
std::mutex and std::timed_mutex are just plain single-owner mutexes.
std::recursive_mutex and std::recursive_timed_mutex are recursive mutexes, so
multiple locks may be held by a single thread.
std::shared_timed_mutex is a read/write mutex.
C++ lock objects
To go with the various mutex types, the C++ Standard defines a triplet of class
templates for objects that hold a lock. These are:

 std::lock_guard<>,
 std::unique_lock<> and
 std::shared_lock<>.

For basic operations, they all acquire the lock in the constructor, and release it in
the destructor, though they can be used in more complex ways if desired.
std::lock_guard<> is the simplest type, and just holds a lock across a critical
section in a single block:
std::mutex m;
void f(){
std::lock_guard<std::mutex> guard(m);
// do stuff
}

std::unique_lock<> is similar, except it can be returned from a function without


releasing the lock, and can have the lock released before the destructor:
std::mutex m;
std::unique_lock<std::mutex> f(){
std::unique_lock<std::mutex> guard(m);
// do stuff
return std::move(guard);
}

void g(){
std::unique_lock<std::mutex> guard(f());
// do more stuff
guard.unlock();
}

See my previous blog post for more


about std::unique_lock<> and std::lock_guard<>.
std::shared_lock<> is almost identical to std::unique_lock<> except that it
acquires a shared lock on the mutex. If you are using
a std::shared_timed_mutex then you can
use std::lock_guard<std::shared_timed_mutex> orstd::unique_lock<std::shared
_timed_mutex> for the exclusive lock,
andstd::shared_lock<std::shared_timed_mutex> for the shared lock.
std::shared_timed_mutex m;
void reader(){
std::shared_lock<std::shared_timed_mutex> guard(m);
// do read-only stuff
}
void writer(){
std::lock_guard<std::shared_timed_mutex> guard(m);
// update shared data
}

Semaphores in C++
The C++ standard does not define a semaphore type. You can write your own with
an atomic counter, a mutex and a condition variable if you need, but most uses of
semaphores are better replaced with mutexes and/or condition variables anyway.
Unfortunately, for those cases where semaphores really are what you want, using a
mutex and a condition variable adds overhead, and there is nothing in the C++
standard to help. Olivier Giroux and Carter Edwards' proposal for
astd::synchronic class template (N4195) might allow for an efficient
implementation of a semaphore, but this is still just a proposal.

Das könnte Ihnen auch gefallen