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
To talk about threads, we first need to know how to manage time and timers.
Provide a strongly-typed, generic and elegant way of programming with time and timers:
chrono
ratio
)#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 !
A
to an instant of a clock B
.std::chrono::steady_clock
: increasing time is guaranted, used for measure of duration (processing times, etc)std::chrono::high_resolution_clock
: increasing time is guaranted with the highest possible time resolution.std::chrono::system_clock
: system clock (i.e convertible from/to a time std::time_t
), but no guarantee to be increasing (i.e in case of a new clock setting)// 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"
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_; }
};
std::this_thread::get_id()
returns the thread UID (OS specific)std::this_thread::sleep_for(const std::chrono::duration<S,U>&)
makes the current thread sleeping for the given duration.std::osyncstream
(introduced in C++20) prevent race condition when accessing output streams std::ostream
. More to come on this.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
std::thread
is a C++ object referencing a system thread (POSIX, Windows, etc).std::thread
(but then thread preemption can change the expected order of execution)std::thread::join()
must be called before the thread
object is destroyed. More to come o this.std::thread::native_handle
if fine tuning is needed (e.g. priority, etc) but then the code becomes OS dependen (i.e portability broken).std::osyncstream
, the output on the console looks like: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
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
Let’s recall few facts about threads:
main()
function.C++11 enforces a proper management of threads by two features:
std::thread
(like std::unique_ptr
, etc): threads cannot be copied but only moved.std::thread
: a thread is joinable if the system thread has not been joined yet (still running or in zombie state).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)
std::thread
object references a system thread, possibly none (default constructor, after a join or a detach, etc).std::thread
object thanks to move semantics, possibly zero (in case of a detached thread).detach()
method breaks the link between the system thread and the std::thread
object that created it.joinable()
method returns true if the std::thread
object is associated with a running system thread or a thread that has terminate but has not been joined yet.std::thread
object is an error and results in calling std::terminate
(default is std::abort
).new
/delete
are thread safe.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
}};
...
jthread
C++20 has introduced std::jthread
. The only two differences with standard std::thread
are
std::jthread
are (weakly) interruptible: the thread can check periodically if another thread requests it to stop and acts accordingly (or not…).std::jthread
: the destructor of the std::jthread
simply requests it to stop and then call join
on itself (auto-join).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