Tutorial 0 & Tutorial 1 QnA

| βŒ› 6 minutes read

πŸ“‹ Tags:


Index

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

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:

Q2: Correction: std::move’s Unspecified State?

Recall std::move. Relevant quote from cppreference:

std::move is used to indicate that an object t may be “moved from”, i.e. allowing the efficient transfer of resources from t 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:

  1. Generally, ownership of moved things belongs to their new handle, so you should only interact with the thing using the new handle
  2. Ownership of a moved thread’s execution belongs to the new handle, not the old one
  3. A moved thread’s old handle is not joinable and detachable
  4. We don’t care about the old handle’s exact state for any moved thing. It depends on compiler implementation.
  5. 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:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <thread>
#include <iostream>
#include <unistd.h>
#include <cassert>

int main(){
    int local = 0;
    std::thread t1([](){
        // do stuff
        sleep(3);
        std::cout << "done!\n";
    });

    std::thread t2 = std::move(t1); // Transfer ownership to t2
    assert(!t1.joinable()); // this passes, meaning t1.joinable() is false.
    // This means that t1 cannot be joined or detached.
    t2.join();
}

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:

  1. You cannot join() or detach() a moved thread
  2. Ownership of the thread’s execution is transferred to the new handle (t2)
  3. 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:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void foo(){
    int local = 0;
    // the usual way
    std::thread t1([local](){
        printf("Local is %d \n", local); // Local is 0
    });  
    t1.join();

    std::thread t2([](int my_local){
        printf("Local is %d \n", my_local); // Local is 0
    }, local); // pass local just like a fn argument
    t2.join();
}

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):

gcc c++11 src code

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.


  1. 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. ↩︎