Thread Classes
- Design a concurrent solution using a threading class library
- Describe how to communicate between tasks executing on different threads
"The use of RAII to control thread resources ... cannot be over-emphasized. RAII is at the center of the design of the C++ thread library and all of its facilities." Paterno (2011)
The C++11 standard added five multi-threading libraries to the Standard Library Collection. Its thread
and future
libraries contain all the templates for simple multi-threaded solutions. The thread
library provides support for creating and managing threads that execute concurrently. The future
library provides support for retrieving the result from a function that has executed in the same or a concurrently executing thread. These libraries implement the Resource Allocation Is Initialization (RAII) idiom.
This chapter describes the thread
class template in detail and demonstrates how to launch and synchronize threads. It also describes how to communicate data between threads using objects generated from the future
class, the promise
class and the packaged_task
class templates as well as functions generated from the async()
function template.
Thread Class
The thread
class defines an object that represents a single thread of execution in a process. The thread class template is defined in the header file <thread>
.
A thread
object is either joinable or not-joinable. A joinable object represents an actual thread of execution with a unique id. A non-joinable object represents a potential thread of execution (the object is not associated yet with a hardware thread, or the thread has been already joined). Operations on a thread object can change its joinable/non-joinable state.
Example
The following program executes three tasks concurrently. It spawns two child threads from its main thread, executes all three tasks and waits in its main thread for the spawned threads to finish.
Each thread performs the same task (task()
). The following program
// Thread Class
// thread.cpp
#include <iostream>
#include <string>
#include <thread>
void task(const std::string& str)
{
std::cout << str + " says Hi\n";
}
int main()
{
// spawn child thread t1
std::thread t1(task, "t1");
// spawn child thread t2
std::thread t2(task, "t2");
// continue executing main thread
task("main");
// synchronize - IMPORTANT!
t2.join();
t1.join();
}
could produce the output:
main says Hi
t1 says Hi
t2 says Hi
The synchronization step is necessary. Had we neglected to join()
the spawned threads to the main thread, the result would be undefined. The main thread could, for instance, finish executing its task and return control to the operating system before one or both of the spawned threads had finished executing their tasks.
Member Functions
The member functions of the templated thread
class include:
thread() noexcept
- creates a non-joinable thread object (a potential thread of execution)thread(thread&& t) noexcept
- moves the thread handler from threadt
to the newly constructed thread object~thread()
- destroys the current thread objectthread& operator=(thread&& t) noexcept
- moves the thread handler from threadt
to the current not-joinable objectthread::id get_id() const
- returns the unique identifier of the current thread objectbool joinable() const noexcept
- returns true if the current object is an actual thread of executionvoid join()
- returns when the current object has completed executing its taskvoid detach()
- detaches the current object from its parent objectvoid swap(thread& t)
- swaps the state of the current object with the state of objectt
This template's copy-constructor and the copy-assignment special member functions are deleted. The thread::id
type represents a thread identifier. The definition of the thread
class includes an overload of the insertion operator for a right operand of type thread::id
.
The class definition also includes a template for constructing objects that execute functions, function objects or lambda expressions. This template takes the form:
template <typename Fn, typename... Args>
explicit thread(Fn&& f, Args&&... args);
Fn
is the type of the function, function object or lambda expression and Args
are the arguments passed to the function call itself. See the following for examples.
Thread Identifier
The thread identifier of a joinable thread
object is accessible from within the function executing the thread's task. std::this_thread::get_id()
returns this identifier.
The following program launches 10 thread objects and displays their identifiers on standard output. The program creates each object through the constructor template with the address of a function as its sole argument:
// Thread Class - Thread Identifiers
// thread_id.cpp
#include <iostream>
#include <thread>
#include <vector>
constexpr int NT = 10;
void task()
{
std::cout << "Thread id = "
<< std::this_thread::get_id() << std::endl;
}
int main()
{
// create a vector of threads
std::vector<std::thread> threads;
// launch execution of each thread
for (int i = 0; i < NT; i++)
threads.push_back(std::thread(task));
// synchronize their execution here
for (auto& thread : threads)
thread.join();
}
A possible output for the program above:
Thread id = Thread id = 23620
Thread id = 23596
Thread id = 23608
23584
Thread id = 23592
Thread id = 23612
Thread id = 23600
Thread id = 23588
Thread id = 23604
Thread id = 23616
Note that a thread does not necessarily pass the entire cascaded insertion expression to standard output as a cohesive unit. Some executing threads interleave the constituent operations among themselves.
The following program uses the templated constructor to launch a task that takes a single argument:
// Thread Class - Function with Arguments
// thread_id_arg.cpp
#include <iostream>
#include <thread>
#include <vector>
constexpr int NT = 10;
void task(int i)
{
std::cout << i << " Thread id = "
<< std::this_thread::get_id() << std::endl;
}
int main()
{
// create a vector of not-joinable threads
std::vector<std::thread> threads;
// launch execution of each thread
for (int i = 0; i < NT; i++)
threads.push_back(std::thread(task, i));
// synchronize their execution here
for (auto& thread : mythreads)
thread.join();
}
A possible output:
0 Thread id = 23968
3 Thread id = 19484
7 Thread id = 21992
2 Thread id = 21656
8 Thread id = 17844
5 Thread id = 23764
4 Thread id = 24396
6 Thread id = 22580
1 Thread id = 24100
9 Thread id = 23964
In this particular run, the output from different threads happens not to interleave.
Function Object
The function object version of the above example is as follows:
// Thread Class - Function Object
// thread_id_fo.cpp
#include <iostream>
#include <thread>
#include <vector>
constexpr int NT = 10;
class Task
{
public:
Task() = default;
void operator()(int i)
{
std::cout << i << " Thread id = "
<< std::this_thread::get_id() << std::endl;
}
};
int main()
{
// create a vector of not-joinable threads
std::vector<std::thread> threads;
// launch execution of each thread
for (int i = 0; i < NT; i++)
threads.push_back(std::thread(Task(), i));
// synchronize their execution here
for (auto& thread : threads)
thread.join();
}
A possible output:
023 Thread id = 25096
1 Thread id = 22704
7 Thread id = 24540
Thread id = 25516
6 Thread id = 17152
4 Thread id = 14460
Thread id = 25024
9 Thread id = 24632
8 Thread id = 18060
5 Thread id = 22796
In this particular run, the output from different threads happens to interleave.
Lambda Expression
The lambda expression version of the above example is more compact and accesses the index i
by value as a non-local variable:
// Thread Class - Lambda Expression
// thread_id_lambda.cpp
#include <iostream>
#include <thread>
#include <vector>
constexpr int NT = 10;
int main()
{
// create a vector of not-joinable threads
std::vector<std::thread> threads;
// launch the execution of each thread
for (int i = 0; i < NT; i++)
threads.push_back(std::thread([=]()
{
std::cout << i << " Thread id = " <<
<< std::this_thread::get_id() << std::endl;
}));
// synchronize their execution here
for (auto& thread : threads)
thread.join();
}
A possible output:
0 Thread id = 23624
3 Thread id = 24412
7 Thread id = 20744
2 Thread id = 9580
8 Thread id = 18552
5 Thread id = 23808
4 Thread id = 24560
6 Thread id = 24296
1 Thread id = 21600
9 Thread id = 19552
Note that the order of output differs from that for the other versions.
future
Library
The future
library of the thread support category facilitates efficient transfer of values between tasks through shared states
. The class and function templates that support communications across a shared state are defined in the header file <future>
.
A future
object retrieves a value that a provider has stored in a shared state. Each provider-future pair establishes a synchronization point for two tasks that are executing concurrently. The provider creates an empty shared state on initialization. Once the provider has supplied a value to that shared state, that state is ready for access by the future
object associated with that provider. A shared state can survive the lifetime of its provider.
Futures
Instantiation of the templated std::future
class creates a future
object. A future
object is either valid or not-valid. A valid object is associated with a shared state and can retrieve the value of that shared state once it is ready. Until it is ready, any retrieval request necessarily waits.
Member Functions
The member functions of the templated future class include:
future() noexcept
- creates a future object not associated with a shared statefuture(future&& f) noexcept
- moves the shared state from futuref
to the current object~future()
- disassociates the shared state from the current object and destroys the state if not associated with any other objectfuture& operator=(future&& f) noexcept
- acquires the shared state of futuref
T get()
- returns the value stored in the current object's shared state, if ready; waits, if not readybool valid() const noexcept
- returns true if the current object is associated with a shared statevoid wait() const
- waits for the current object's shared state to be ready
This class' copy-constructor and the copy-assignment special member functions are deleted.
Providers
A provider object complements a future object. One of the following templates can instantiate a provider object:
std::promise
class templatestd::packaged_task
class templatestd::async()
function template
promise
Objects
A promise
object creates or acquires a shared state in which it can store a value.
The templated promise
class defines a simple set_value()
counterpart to the get()
member function of the templated future
class. The member functions of the templated promise
class include:
promise()
- a promise object with a new empty shared statepromise(promise&& p) noexcept
- moves the shared state from promisep
to the newly created current object~promise()
- abandons the shared state and destroys the promisepromise& operator=(promise&& p) noexcept
- moves the shared state from promisep
to the current objectfuture<T> get_future()
- returns the future object associated with the current object's shared statevoid set_value(const T&)
- stores a value in the current object's shared state, making it ready for retrievalvoid swap(promise& p)
- swaps the shared state of the current object with the shared state of objectp
This template's copy-constructor and the copy-assignment operations are deleted.
packaged_task
Object
A packaged_task
object consists of two components: a stored task and a shared state.
The templated packaged_task
class defines a simple wrapper for passing the result of a task to a future
object; that is, for launching a thread that executes a task and capturing the return value, which a future
can subsequently retrieve. The member functions of the templated packaged_task
class include:
packaged_task()
- a packaged_task object with no shared state and no taskpackaged_task(packaged_task&& p) noexcept
- moves the shared state and stored task from packaged_taskp
to the newly created current object~packaged_task()
- abandons the shared state and destroys the packaged_taskpackaged_task& operator=(packaged_task&& p) noexcept
- acquires the shared state and stored task from packaged_taskp
bool valid() const noexcept
- returns true if the current object is associated with a shared state and a stored taskfuture<T> get_future()
- returns the future object associated with the current object's shared statevoid operator()(Args...)
- forwards the function arguments of typeArgs
to the stored task and initiates its executionvoid swap(packaged_task& p)
- swaps the shared state of the current object with that of objectp
This template's copy-constructor and copy-assignment operations are deleted.
async()
function
The templated async()
function provides an extremely simple pair that spawns a thread to execute a task and creates a future for retrieving the return value from that task.
An async()
function
- accepts the address of the function that defines the task
- launches the task
- reverts control to its caller
- returns a
future
object that can retrieve the value returned on the task's completion
The executing task stores the return value temporarily in a shared state. The future
object can retrieve the value from this shared state.
The template for an async()
function has the form
template<class Fn, class... Args>
future<typename result_of<f(Args...)>::type> async(Fn&& fn, Args&&... args);
fn
is a pointer to a function or any kind of move-constructable function object of type Fn
and args
denotes arguments of type Args
to the function, where type is also move-constructable.
Examples
Promise - Future
Fulfilling a promise on a child thread and retrieving the promised value on the main thread involves passing the promise object by reference to the thread. In the following program task()
on thread t
fulfills the promise by setting a value. The main thread retrieves that value.
// Promise - Future
// promise_future.cpp
#include <iostream>
#include <thread>
#include <future>
void task(std::promise<double>& p)
{
p.set_value(12.34);
}
int main()
{
std::promise<double> p;
std::future<double> f = p.get_future();
std::thread t(task, std::ref(p));
std::cout << "Value = " << f.get()<< std::endl;
t.join();
}
Value = 12.34
Note that any return value from the function executed by a child thread can be captured by an std::promise
object. The return value from the function is otherwise ignored.
Packaged Task
The following program packages a task and a shared state. The get_future()
member function retrieves the future object that will hold the return value generated by execution of the task
. Calling the packaged_task executes the task
, while calling the get()
member function on the future
object retrieves the return value, which the main function can then display:
// Packaged Task
// packaged_task.cpp
#include <iostream>
#include <thread>
#include <future>
double task(double x) { return x * 2; }
int main()
{
std::packaged_task<double(double)> pt(task);
auto f = pt.get_future();
pt(10);
double r = f.get();
std::cout << "Result = " << r << std::endl;
}
Result = 20
async()
The following program launches task()
asynchronously and returns the future object associated with the shared state. The get()
member function on the future object retrieves the value of the shared state, which the main function can then display:
// Asynchronous Launch
// async.cpp
#include <iostream>
#include <thread>
#include <future>
double task(double x) { return x * 2; }
int main()
{
std::future<double> f = std::async(task, 10);
double r = f.get();
std::cout << "Result = " << r << std::endl;
}
Result = 20
On the three examples, this is the simplest.
A More Complex Use Case
The following program demonstrates changes in the validity of future objects as a result of its operations on them. The program:
- creates 2
future
objects - an invalid one (using the default constructor), and a valid one - moves the shared state of the
future
object created by asychronously launching the execution ofget()
- moves the shared state from the valid
future
object (g
) to the not-valid one (f
) - retrieves the value from the shared state by calling the
get()
member function on the validfuture
object (f
)
// Future Class Template - Explicit Asynchronous Launch
// future_async.cpp
#include <iostream>
#include <future>
double get() { return 12.34; }
int main()
{
std::future<double> f; // default ctor
std::future<double> g = std::async(get); // move-ctor
std::cout << "After Construction" << std::endl;
std::cout << (f.valid() ? "f is valid" : "f is not valid") << std::endl;
std::cout << (g.valid() ? "g is valid" : "g is not valid") << std::endl;
f = std::move(g); // move-assignment
std::cout << "After Assignment" << std::endl;
std::cout << (f.valid() ? "f is valid" : "f is not valid") << std::endl;
std::cout << (g.valid() ? "g is valid" : "g is not valid") << std::endl;
double a = f.get(); // retrieve shared value
std::cout << "After Retrieval" << std::endl;
std::cout << (f.valid() ? "f is valid" : "f is not valid") << std::endl;
std::cout << (g.valid() ? "g is valid" : "g is not valid") << std::endl;
std::cout << "Return Value = " << a << std::endl;
}
After Construction
f is not valid
g is valid
After Assignment
f is valid
g is not valid
After Retrieval
f is not valid
g is not valid
Return Value = 12.34
Note that after retrieval of the value of a shared state associated with a future object that object is no longer valid.
Thread Local Storage
The same variable can have a different storage location for each thread in a team of threads. We identify such variables as having thread_local
storage duration. This storage duration lasts the lifetime of that thread and is the equivalent of static storage duration for a program variable.
In the following example, k
has three separate storage locations: one for the main thread, one for thread t1
and one for thread t2
:
// Thread Local Storage Duration
// thread_local.cpp
#include <iostream>
#include <sstream>
#include <thread>
thread_local int k = 0;
void task(int i)
{
k = i;
std::stringstream s;
s << k << " at " << &k << std::endl;
std::cout << s.str();
}
int main()
{
k = 10;
std::thread t1(task, 15);
std::thread t2(task, 20);
t1.join();
t2.join();
task(k);
}
15 at 00A6730C
20 at 00A67CCC
10 at 00A5644C
Note that the address of the storage location for k
is different for each thread.