License : Creative Commons Attribution 4.0 International (CC BY-NC-SA 4.0)
Copyright : Frédéric Pennerath, CentraleSupelec
Last modified : April 19, 2024 10:22
Link to the source : threads.md

Table of contents

Time management

To talk about threads, we first need to know how to manage time and timers.

Key points

Provide a strongly-typed, generic and elegant way of programming with time and timers:

Durations

#include <chrono>  // Header file to manage times and timers

// A duration is a multiple of a time unit encoded as a fraction of seconds.

// For example the next duration type will contain multiples of hundredths of a second
using hundredths_t = std::chrono::duration<long,std::ratio<1,100>>;  // std::ratio<1,100> encodes fraction 1/100

hundredths_t t {100};  // t is 100/100 = 1 second
t = 10;                // Error : 100 has no unit
long n = t.count();    // n is equal to 100

// Many convenient type alias (C++11) and literals (C++14) are available

// First let us import chrono literals into the root namespace.
using namespace std::literals;        // Import all literals
using namespace std::chrono_literals; // Import only chrono literals
using namespace std::chrono;          // Import all members of namespace std::chrono
    
std::chrono::nanoseconds  nanos    = 1ns; // Equivalent to std::chrono::duration<long long,std::nanos>
std::chrono::microseconds micros   = 1us;
std::chrono::milliseconds millis   = 1ms;
std::chrono::seconds      seconds  = 1s;
std::chrono::minutes      minutes  = 1min;
std::chrono::hours        hours    = 1h;

// Implicit conversions are strongly typed and safe:

// 1) Comparisons take into account time units:
static_assert(1000ms == 1s);
static_assert(59min < 1h);

// 2) No loss of resolution, unless explicit, is possible:
minutes = heures; // Ok (implicit conversion of hours to minutes)
hours = minutes;  // Error (implicit conversion from minutes to hours is forbidden)

// Arithmetic calculations are strongly typed and safe as well:
millis = 1h + 2min - 1s;     // Ok
millis = 2 * 1h + 3min / 2;  // Ok
++millis;                    // Ok
millis = 1h + 1;             // Nok
millis = 1h * 1s;            // Nok

// Only explicit conversion allows to lose precision:
std::cout <<std::chrono::duration_cast<std::chrono::seconds>(1200ms).count(); // Display 1 !

Instants and clocks

// Ecvery clock has nested types providing instant and duration types
std::chrono::steady_clock::time_point tBegin, tEnd;
std::chrono::steady_clock::duration duration;

// Every clock provides a methdo now() to provide the current time point
tBegin = std::chrono::steady_clock::now();

// Sleep for one second (sleep_for expects a duration)
std::this_thread::sleep_for(1s);

tEnd = std::chrono::steady_clock::now();

// The clock is steady: we are sure that tEnd is larger than tBegin
assert(tBegin <= tEnd);

// Compute the duration elapsed between both instants
duration = tEnd - tBegin;

// The time unit of an instant can only be modified using std::chrono::time_point_cast
auto system = std::chrono::time_point_cast<std::chrono::seconds>(std::chrono::system_clock::now());
auto steady = std::chrono::time_point_cast<std::chrono::seconds>(std::chrono::steady_clock::now());

// Two instants given by two different clocks are incompatible (even using the same unit)
auto duration1 = system - system;  // Compiles
auto duration2 = system - steady;  // Does not compile

// Instants of the system clocl are the only one that can provide a "readable" date
// as the number of seconds elapsed since 1st January, 1970
std::time_t t = std::chrono::system_clock::to_time_t(system); 
std::cout << std::asctime(std::localtime(&t)) // Display a date like "Thu Jan 01 12:00:00 2015"

Thread management

Let us first introduce a class Task with a name and a duration, to simulate some computation:

#include <iostream>
#include <chrono>
#include <thread>
#include <syncstream>

class Task {
  std::string desc_;                     // Name of the task
  std::chrono::milliseconds duration_;   // Duration in ms
  bool done_;                            // Flag true if task is done

public:
  Task(std::string desc, const std::chrono::milliseconds& duration): desc_(desc), duration_(duration), done_(false) {}

  void run() {
    // Ensure synchronous access to std::cout to prevent race condition between threads
    std::osyncstream(std::cout) << "Thread " << std::this_thread::get_id() << " is starting task "
    << desc_ << " (duration: " << duration_.count() * 1.E-3 << "s.)" << std::endl;

    // Make the thread sleeping for the specified duration
    std::this_thread::sleep_for(duration_);
    
    done_ = true; // Task is done

    std::osyncstream(std::cout) << "Thread " << std::this_thread::get_id() << " has completed task " << desc_ << std::endl;
  }

  bool is_done() const { return done_; } 
};

Remarks:

Launching threads

Now let’s illustrate the various ways to launch threads:

#include <iostream>
#include <functional>
#include <thread>

using namespace std::literals::chrono_literals;

void run_task_A() { Task("A", 1s).run(); }
void run_task(std::string desc, std::chrono::milliseconds duration) { Task(desc, duration).run(); }

int main() {
  // Passing a function pointer
  std::thread thread1 { &run_task_A };

  // Pasing a function pointer with arguments
  std::thread thread2 { &run_task, "B", 2s };

  // Passing a method pointer
  std::thread thread3 { &Task::run, Task{"C", 1s} };

  // Passing a lamba function, (beware of scopes of captured variables)
  Task t { "D", 4s };
  std::thread thread4 { [&t] () -> void { t.run(); } };

  // Passing a function wrapper
  std::thread thread5 { std::function<void (std::string, std::chrono::milliseconds)> { run_task }, "E", 3s };

  // Wait all threads have completed
  thread1.join();
  thread2.join();
  thread3.join();
  thread4.join();
  thread5.join();

  std::cout << "All threads are finished." << std::endl;
}

The console output looks like (Linux):

Thread 139706069243648 is starting task C (duration: 1s.)
Thread 139706086029056 is starting task A (duration: 5s.)
Thread 139705926633216 is starting task D (duration: 4s.)
Thread 139706077636352 is starting task B (duration: 2s.)
Thread 139706060850944 is starting task E (duration: 3s.)
Thread 139706069243648 has completed task C
Thread 139706077636352 has completed task B
Thread 139706060850944 has completed task E
Thread 139705926633216 has completed task D
Thread 139706086029056 has completed task A
All threads are finished

Remarks:

Thread 140676004595456 is starting task A (duration: 5s.)
Thread 140675996202752 is starting task B (duration: Thread 2s.)
140675987810048 is starting task C (duration: Thread 140675979417344 is starting task D (duration: 1s.)
Thread 140675971024640 is starting task E (duration: 3s.)
4s.)
Thread 140675987810048 has completed task C
Thread 140675996202752 has completed task B
Thread 140675971024640 has completed task E
Thread 140675979417344 has completed task D
Thread 140676004595456 has completed task A
All threads are finished

Passing arguments by address

For safety, function arguments are passed by value to the thread. Passing by address requires explicitly to use a reference wrapper std::reference_wrapper<T> returned by std::ref:

    std::cout << std::boolalpha;

    Task task1("A", 1);
    std::thread thread1(&Task::run, task1);
    thread1.join();
    std::cout << "Is task1 done? " << task1.is_done() << "\n" << std::endl;

    Task task2("B", 1);
    std::thread thread2(&Task::run, std::ref(task2));
    thread2.join();
    std::cout << "Is task2 done? " << task2.is_done() << std::endl;

The output gives

Thread 140329823733504 is starting task A (duration: 1s.)
Thread 140329823733504 has completed task A
Is task1 done ? false

Thread 140329823733504 is starting task B (duration: 1s.)
Thread 140329823733504 has completed task B
Is task2 done ? true

Management of thread life-cycle

Reminder

Let’s recall few facts about threads:

How C++11 enforces the join policy

C++11 enforces a proper management of threads by two features:

Here are some examples:

  // Joinable state of a running thread
  std::thread thread1 { &Task::run, Task{"A", 1s } }; // A running thread is joinable
  assert(thread1.joinable());                         // We can check it using method joinable()

  // Default constructor
  std::thread thread2 {};        // Object thread is empty
  assert(! thread2.joinable());  // An empty thread object is not joinable

  // Move a running system thread from one thread to another
  thread2 = std::move(thread1);  // Thread state is moved with the system thread
  assert(! thread1.joinable());  // thread1 is not joinable anymore 
  assert(  thread2.joinable());  // but thread2 gets joinable now

  // Detaching a system thread
  std::thread thread3 { &Task::run, Task{"B", 3s} };
  thread3.detach();              // detach() breaks the link from thread3 to the underlying system thread
  assert(! thread3.joinable());  // so thread3 is not joinable anymore

  thread3 = std::move(thread2);  // Would have caused an error if detach() wasn't called
  assert(thread3.joinable());    // thread3 is joinable again

  // Joining a thread
  thread3.join();                // join() waits a thread terminates
  assert(! thread3.joinable());  // After returning froma join, the thread is not joinable anymore

  std::cout << "End of the main thread (computation of B is not finished)" << std::endl;
  // Destruction of thread1, thread2, thread3 occurs when leaving the scope.
  // If one was still joinable, std::terminate() and then std::abort() would have been called

The standard does not say what should happen to a system thread if the main thread (i.e the process) terminates. With POSIX, all pthreads are terminated:

Thread 140097337984768 is starting task B (duration: 3s.)
Thread 140097346377472 is starting task A (duration: 1s.)
Thread 140097346377472 has completed task A
End of the main thread (computation of B is not finished)

Summary:

Memory management and thread local variables

Reminder:

Thread local variables

Thread locales (thread_local keyword) are thread-specific global variables:

See the next example for generating random numbers from different threads.

#include <random>                           // C++11 random generators
...
thread_local std::random_device gen{};      // Non determinist random generator: secure but slow
thread_local std::mt19937 pseudogen{gen()}; // Pseudo random generator: fast but "determinisit"
thread_local std::normal_distribution<double> normal{0.,1.};  // Normal distribution of mean 0 and variance 1
...
std::thread t1 {
  [] () {
    std::cout << normal(pseudogen) << std::endl;
}};

std::thread t2 {
  [] () {
    std::cout << normal(pseudogen) << std::endl;  // Not using the same pseudogen object as t1
}};
...

The expert’s corner: jthread

C++20 has introduced std::jthread. The only two differences with standard std::thread are

Here is an example:

void worker_task(std::stop_token stoken) {
  // Functions run by jthread expect a std::stop_token argument

  std::cout << "I'm working..." << std::endl;

  // Make some endless computation until a stop is requested
  while(! stoken.stop_requested()) {
    // Do something
    std::this_thread::sleep_for(100ms);
  }
  
  std::cout << "I'm requested to stop." << std::endl;
}

{
  // Starts a jthread
  std::jthread worker(worker_task);

  std::this_thread::sleep_for(2s);
  // Here the destructor of the jthread is called
}
// We are sure from here that worker_task exited properly
Frédéric Pennerath,