Index
- Index
- Q1: Different lock types
- Q2: Correction:
std::move
’s Unspecified State? - Q3: Lambdas - why don’t we use the argument list () instead of capture lists []?
- Q4: Why do spurious wakeups happen?
Q1: Different lock types
What’s the difference between lock_guard, scoped_lock and unique_lock? It seems like scoped_lock is similar lock_guard, but it can acquire multiple mutexes together, so is there a need to use lock_guard?
The differences were covered in the lecture (based on the slides), but I’ll summarise them here.
lock_guard
is just a RAII wrapper over a mutex.scoped_lock
is a lock_guard, just that you can lock/unlock multiple mutexes in a deadlock-free way.unique_lock
is a lock_guard on steroids. In addition to lock_guard’s basic feature, it…- can defer locking:
unique_lock lk{mtx, std::defer_lock}
- enables manual locking and unlocking
- is the only1 lock that can be used with
condition_variable
to build things like Monitors - can swap state with another unique_lock
- can defer locking:
TLDR:
Lock | Main Feature | Usecase |
---|---|---|
lock_guard | Simple RAII lock. | You just need a simple RAII wrapper |
scoped_lock | Lock multiple mutexes deadlock-free | You need multiple mutexes in your critical section |
unique_lock | Flexible locking (deferable, moveable) | You need more control over locking behaviour |
The use of different RAII locks really depends on your usecase.
Specifically, is there a need to use lock_guard?
No. You can achieve lock_guard functionality with unique_lock. You can also just pass one mutex to scoped_lock to achieve the same functionality as lock_guard.
BUT – lock_guard is not entirely useless. Sometimes you want to limit the amount of features a data structure gives you to avoid shooting youself in the foot.
Relevant further readings:
- SO: lock_guard vs scoped_lock
- SO: is unique_lock slower?
- Community Blogpost: Why I still use lock_guard
Q2: Correction: std::move
’s Unspecified State?
Recall std::move. Relevant quote from cppreference:
std::move
is used to indicate that an objectt
may be “moved from”, i.e. allowing the efficient transfer of resources fromt
to another object.
There was confusion over the state of the moved thing. The confusion is regarding the docs' vague phrasing:
Unless otherwise specified, all standard library objects that have been moved from are placed in a “valid but unspecified state”, meaning the object’s class invariants hold…
The general questions from this are:
- What is inside the moved thread’s old handle?
- What can we do with the old identifier for thread?
These are good questions because it highlights the difference between language standards and implementation.
TLDR WHAT YOU SHOULD KNOW ABOUT MOVING:
- Generally, ownership of moved things belongs to their new handle, so you should only interact with the thing using the new handle
- Ownership of a moved thread’s execution belongs to the new handle, not the old one
- A moved thread’s old handle is not joinable and detachable
- We don’t care about the old handle’s exact state for any moved thing. It depends on compiler implementation.
- Interacting with the old handle should not affect the thing that is in the new handle
Correction: moved thread is not joinable
I re-read the specs and tested on a minimal example. You cannot join()
or detach()
on the moved thread’s old handle:
|
|
C++ Implementations of std::move affects its internal state
What is the old handle’s state?
Many asked this because we want to know what we can do with t1 (in the above example).
What std::move
exactly does to the moved thing (t1)’s internal state is compiler dependent.
Different compilers implement c++ functions differently, as long as the behaviour conforms to the c++ standard.
So, the exact state of the moved thing’s old handle depends on how the compiler library implements it.
This stackoverflow answer explains how different implementations results in different internal states of the moved thing’s old handle. This answer was in the context of std::vector
. Unfortunately, I couldn’t find the precise difference for std::thread
in my gcc and clang compilers.
So, “what is the exact internal state” doesn’t really matter.
In the context of std::thread
and this class, you just need to know that:
- You cannot join() or detach() a moved thread
- Ownership of the threadβs execution is transferred to the new handle (t2)
- Interacting with the old handle should not affect the thing that is in the new handle
Q3: Lambdas - why don’t we use the argument list () instead of capture lists []?
You can use the argument list instead of capture lists, but the ergonomics is not as good (my opinion).
The following will compile. Test on your own machine:
|
|
Q4: Why do spurious wakeups happen?
The reason I gave in tutorial might not be satisfying. This section is for those who want resources for a deeper explanation (which is very out of syllabus π).
β οΈ Otherwise, for this class, you can just take it as ‘they just happen’. β οΈ
The simple answer: It is allowed by POSIX
Under the hood (for Linux), condition_variable uses pthread’s condition variable: pthread_cond_t
.
You can verify that pthread is used under the hood in your own compiler library in Linux. For my compiler (gcc):

So what? Spurious wakeups are defined behaviours in the IEEE POSIX standard.
Relevant quote:
… POSIX.1-2024 explicitly documents that spurious wakeups may occur.
The linked doc’s section on “Rationale -> Multiple Awakenings by Condition Signal” section is good. They provided an example of spurious wakeups and explained why they allow this ‘bug’ in the POSIX standard.
We know that the POSIX standard just allows spurious wakeups (‘Its a feature not a bug’).
So we should expect spurious wakeups for condition_variable
and wrap it in a while loop to deal with such wakeups.
Not satisfied?
For your own exploration; This blogpost dives much much deeper, enumerating different reasons (such as scheduling) specific to pthreads. You don’t need to know any of these for CS3211.
-
For condition_variable_any, this does not apply. Any lock that implements Basic Lockable can be used for this. Follow the links to find out more. ↩︎