To perform thread/task parallelization
until 2011, C++ don’t have MT support
C++11:
std::thread
At runtime, the main thread runs main()
. std::thread
is used to add more threads.
// Functional object callable type
class CallableHello {
public:
void operator()() const {
cout << "callable hey\n";
}
}
// function ptr
void hello() { cout << "hey\n"; }
int main() {
// Thread as function ptr
std::thread t1(hello);
// Thread as callable object
CallableHello ch;
std::thread t2(ch);
t1.join(); t2.join();
}
main()
std::thread t(funcptr)
, t.join()
to join
()
operator)Note that CallableHello()
is a function pointer type!
Hence t1 is actually a function declaration because of the type.
std::thread
is the return type
// function decl of return type thread
std::thread t1(CallableHello()); // FUNCTION DECLARATION
// return-type name(param-type);
std::thread t2{CallableHello()}; // THREAD INITIALIZATION
std::thread t2_alt((CallableHello())); // THREAD INITIALIZATION
std::thread t4([]{ cout << "ok\n"; }); // THREAD INITIALIZATION
std::thread t4_alt([](){ cout << "ok\n"; }); // THREAD INITIALIZATION
int main() {
t2.join(); // OK!
t3.join(); // OK!
t4.join(); // OK!
// t1.join(); // CANNOT RUN!
}
use std::ref
as a reference instead of &
open a curly bracket, close a curly bracket: that’s a scope in between the brackets.
join()
exactly once per std::thread
joinable()
to check if thread is donetry { ... } catch(...) { t.join(); throw; } t.join()
make sure error thread joined!joinable()
is false after calling detach.Owner: object with a pointer to a memory allocation created
by new
(on heap) and must be delete
d.
Every object on free store (dynamic memory, heap) has exactly one owner. If multiple pointers, only 1 is the owner. Objects on stack do not have owners.
Aside: The const
key word can be placed in multiple places:
const int m1 = 100; // m1 is an int that is constant
int const m1_alt = 100; // same as m1; m1_alt is a constant int
int meme = 42069;
const int& n1 = meme; // n1 is a reference to an int that is constant
int const& n2 = meme; // n2 is a reference to a constant int
const int* n3 = &meme; // n3 is a ptr to an int that is constant
int const* n4 = &meme; // n4 is a ptr to a constant int
int *const n5 = &meme; // n5 is a constant ptr to an int
// meme doesn't have to be constant!
meme = 5; // ok!
*n5 = 10; // ok!
// NOT ALLOWED:
// n1 = 10; n2 = 10;
// *n3 = 10; *n4 = 10;
int c = 0;
void foo(int i, std::string const& s); // must be string const&
void oops() {
char buf[1024];
std::thread t(foo, 3, buf); // WILL NOT COMPILE
// unless the variable is wrapped e.g.
// std::ref(buf), buf will be COPIED and CASTED to t
assert c == 0;
t.detach();
}
void pass_by_ref_ok() {
char buf[1024];
std::thread t(foo, 3, std::ref(buf));
assert c == 1; // REFERENCE modified
t.detach();
}
void pass_by_val_ok() {
char buf[1024];
std::thread t(foo, 3, std::string(buf));
// buf is casted to string first before being passed by val as param
assert c == 0;
t.detach();
}
oops
will never compile as main may detach t before the buf
is converted to std::string
.
foo
may never runthread t (foo, 3, std::string(buf));
enter scope = allocate and initialize (on heap if necessary).
exit scope = deallocate and destroy (from heap if necessary).
Start-of-life begins after
End-of-life
std::thread
instances own resource.
Instances of std::thread
are movable (change owner)
and not copyable.
Transfer of ownership can be done with the idioms:
// TRANSFER OUT, no parameters
std::thread f() {
void func();
return std::thread(func);
}
// TRANSFER OUT, with parameters
std::thread g() {
void func(int);
return std::thread(func, 42);
}
void moveHere(std::thread t);
// TRANSFER IN
void g() {
void func();
moveHere(std::thread(func));
std::thread t(func);
moveHere(std::move(t));
}
l-values: Values to the left of the assign operator. Evaluation determines the identity of an object/bit-field/function.
r-values: Values to the right of the assign operator. Evaluation initializes an object/bit-field computed from the expression.
C++11 has r-value references with declarator &&
.
They bind to rvalues and not to lvalues.
Distinguish r-values from l-value references with declarator&
Transfer of resources from temporary objects to l-values.
This is useful for std::thread
, std::unique_lock
, std::unique_ptr
and other such classes to move their resources around
and prevent multiple copies.
See the examples below from Microsoft.
// Move constructor.
MemoryBlock(MemoryBlock&& other) noexcept
: _data(nullptr)
, _length(0)
{
// Copy the data pointer and its length from the
// source object.
_data = other._data;
_length = other._length;
// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
other._data = nullptr;
other._length = 0;
}
// Move assignment operator.
MemoryBlock& operator=(MemoryBlock&& other) noexcept
{
if (this != &other)
{
// Free the existing resource.
delete[] _data;
// Copy the data pointer and its length from the
// source object.
_data = other._data;
_length = other._length;
// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
other._data = nullptr;
other._length = 0;
}
return *this;
}
Race condition: a semantic flaw occuring when timing of events affects correctness.
Data race: Undefined behaviour caused by two memory accesses where
Wrap data structure in a protection mechanism.
std::mutex
unlock
on the same mutex synchronize with the next lock
.futex()
linux syscallfutex
’s release-acquire semantics ensures synchronises-with.lock_guard
: locks a single mutex and unlocks at end of lifetime
lock()
and unlock()
methodsscoped_lock
unique_lock
: doesn’t always own the mutex it is associated with! it may be moved around
std::defer_lock_t
tells the compile to lock later.std::try_to_lock
lock
and unlock
the unique lock yourself (what makes unique lock more powerful than the others.)lock
: locks multiple mutexes
// spin-lock implementation
bool flag; // variable to check for
mutex m;
void wait_for_flag() {
std::unique_lock<std::mutex> lk(m);
while (!flag) {
lk.unlock(); // release to others
wait(1000ms);
lk.lock(); // obtain mutex again
}
}
The above is a form of busy-waiting, which if run for too long may prevent other threads from running. Instead we can use the synchronization primitive conditional variable.
A cond_var is associated with a condition to be met.
cv.wait(lk, []{ return conditional; })
is called by a thread.
true
cv.notify_one()
or cv.notify_all()
to unblock one or all waiting threads.Spurious wakes and Notify both must be supported.
// ...
std::conditional_variable data_cond;
void thread1() {
while (true) {
std::unique_lock lk(mut);
// The below is an example of a monitor implementation
// while (!data_queue.empty()) { data_cond.wait(lk); }
data_cond.wait(lk, []{ return !data_queue.empty(); });
// Crit. S.
lk.unlock();
// DO OTHER STUFF
}
}