Beruflich Dokumente
Kultur Dokumente
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:
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:
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
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
}
void g(){
std::unique_lock<std::mutex> guard(f());
// do more stuff
guard.unlock();
}
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.