- When apply multithreading
- When do not apply multithreading
- Basic operations on threads
- Passing arguments to thread function
- Exceptions in threads
- Puting thread into sleep
- Niebezpieczeństwa podczas używania wielowątkowości
- Thread sanitizer
- Debugger
- Mutexy
- Menadżery blokad
- Aktywne czekanie (spinlock)
std::condition_variable
std::condition_variable_any
- Zagrożenia dla
std::condition_variable
/std::condition_variable_any
- Divide work between cores
- Improvement of performance
- Parallelize task (division the task into smaller)
- Parallelize data (run same task on multiple data chunks)
- When to many parallel tasks can deteriorate overal performance (cost of creating thread, cost of resources - every thread consume memory)
- When code complexity and effort to introduce threading is higher than performance gain (maintenance cost vs gain)
Item | Description |
---|---|
std::thread name(function) |
|
std::thread::joinable() |
|
std::thread::join() |
|
std::thread::detach() |
|
std::thread::hardware_concurrency() |
|
std::this_thread::get_id() |
|
#include <thread>
#include <iostream>
struct Bar1 {
void operator()() { std::cout << "Hello World!"; }
};
struct Bar2 {
void foo() { std::cout << "Hello World!"; }
};
void foo() {
std::cout << "Hello World!";
}
int main() {
std::thread t1([]() { std:: cout << "Hello World!"; });
std::thread t2(foo);
// pass the function
std::thread t3(&foo);
// pass the function reference
Bar1 bar1;
std::thread t4(bar1);
// overloaded function call operator
std::thread t5(*foo);
// pass the function pointer
Bar2 bar2;
std::thread t6(&Bar2::foo, &bar2);
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
t6.join();
}
/*
ThreadGuard use reference to thread. When execution reach end of scope destructor checks
whether `joinable()` is `true`. If yes then join the thread. Move semantics may be used
to move the thread.
*/
class ThreadGuard
{
std::thread &t;
public:
explicit ThreadGuard(std::thread& t_) : t(t_) { }
~ThreadGuard() {
if (t.joinable())
t.join();
}
};
void fun() { }
int main()
{
std::thread t(fun);
ThreadGuard g(t);
// or ThreadGuard(std::thread(fun));
}
Struct SomeStruct { };
void bar(int x, std::string str, SomeStruct obj) { }
int main() {
std::thread t(bar, 10, "String", SomeStruct{});
t.join();
}
If function expect value and get reference then:
- create variable copy
- and pass reference to copy (so other threads will not see changes on refered object)
To be able to use reference in threads wrapper std::ref
should be introduced (or std::cref()
for const reference).
void bar(int& x, int* y) {
std::cout << "Inside fun: = " << x << " | y = " << *y << std::endl;
x = 20;
*y = 30;
}
int main() {
int x = 10;
int y = 10;
std::thread t(bar, std::ref(x), &y);
t.join();
std::cout << "Outside fun: x = " << x << " | y = " << y << std::endl;
return 0;
}
Warning! We should assure that time of refered variable is longer than thread itself
Example:
#include <thread>
void do_sth([[maybe_unused]] int i) { /* ... */ }
struct A {
int& ref_;
A(int& a) : ref_(a) {}
void operator()() {
do_sth(ref_); // potential access to
// a dangling reference
}
};
std::thread create_thread() {
int local = 0;
A worker(local);
std::thread t(worker);
return t;
} // local is destroyed, reference in worker is dangling
int main() {
auto t = create_thread(); // Undefined Behavior
auto t2 = create_thread(); // Undefined Behavior
t.join();
t2.join();
return 0;
}
- Class method is run in thread as parameter and takes pointer to object that should be called on
std::thread t(&Car::setData, &toyota, 2015, "Corolla")
despite that finction declaration ofsetData()
isvoid setData(int year, const string & model)
Corolla
does not requirestd::ref()
because it is temporary variable (simillar toconst &
)- Method (function, lambda or functor) is copied to thread memory
- Paramethers are copied or moved
void f(int i, std::string const& s);
void oops(int arg)
{
char buffer[1024];
sprintf(buffer, "%i", arg);
std::thread t(f, 3, buffer);
t.detach();
}
Implicit conversion from char[]
to string and detach()
. Conversion could be slowlier than detach()
and we lost reference object.
void f(int i, std::string const& s);
void oops(int arg)
{
char buffer[1024];
sprintf(buffer, "%i", arg);
std::thread t(f, 3, std::string(buffer));
t.detach();
}
- An exception can be only catch in the same thread where was thrown
- To handle exception in other thread we should use pointer on exception:
std::exception_ptr
- The thread that throw exception assigns to pointer
std::current_exception()
- The thread that should handle has to check
std::current_expection() != 0)
. If true, then current thread rethrows exception withstd::rethrow_exception()
- The thread that throw exception assigns to pointer
- Usage of
noexcept
can be considered
#include <iostream>
#include <thread>
#include <exception>
#include <stdexcept>
int main()
{
std::exception_ptr thread_exception = nullptr;
std::thread t([](std::exception_ptr & te) {
try {
throw std::runtime_error("WTF");
} catch (...) {
te = std::current_exception();
}
}, std::ref(thread_exception));
t.join();
if (thread_exception) {
try {
std::rethrow_exception(thread_exception);
} catch (const std::exception & ex) {
std::cout << "Thread exited with an exception: " << ex.what() << "\n";
}
}
return 0;
}
Item | Description |
---|---|
#include <chrono> |
|
using namespace std::chrono_literals |
|
std::this_thread::sleep_until() |
|
std::thread::sleep_for() |
|
#include <chrono>
#include <iostream>
#include <thread>
int main()
{
using namespace std::chrono_literals;
std::cout << "Hello waiter\n" << std::flush;
auto start = std::chrono::high_resolution_clock::now();
std::this_thread::sleep_for(2s);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> elapsed = end - start;
std::cout << "waited " << elapsed.count() << " ms\n";
}
Wyjście:
>> Hello waiter
>> Waited 2000.12 ms
Zjawisko | Opis |
---|---|
Deadlock |
|
Race conditions |
|
Data races |
|
Zagłodzenie procesu/wątku |
|
Livelock |
|
#include <thread>
#include <mutex>
using namespace std;
class X {
mutable mutex mtx_;
int value_ = 0;
public:
explicit X(int v) : value(v) {}
bool operator< (const X & other) const {
lock_guard<mutex> ownGuard(mtx_);
locK_guard<mutex> otherGuard(other.mtx_);
// Rozwiazanie:
// std::scoped_lock l(mtx_, other.mtx_);
return value_ < other.value_;
}
};
int main() {
X x1(5);
X x2(6);
thread t1([&](){ x1 < x2; });
thread t2([&](){ x2 < x1; });
t1.join();
t2.join();
return 0;
}
- Słowo kluczowe
mutable
przy mutexie oznacza tyle, że nawet w przypadku metodyconst
stan mutexumtx_
może zostać zmieniony (wywoływanielock()
iunlock()
to w pewnym sensie jego modyfikacja) - Słowo kluczowe
explicit
przy konstruktorze blokuje niejawne konwersje- Więcej informacji o
explicit
:
- Więcej informacji o
void abc(int &a) { a = 2; }
void def(int &a) { a = 3; }
int main()
{
int x = 1;
std::thread t1(abc, std::ref(x));
std::thread t2(def, std::ref(x));
t1.join();
t2.join();
std::cout << x << std::endl;
}
>> g++ 1.cpp -lpthread -fsanitize=thread -O2 -g
>> ./a.out
-g
- informacja dla debuggera
Jeśli nie ma "niebezpieczeństw", thread sanitizer nie pokaże "raportu".
>> g++ 1.cpp -lpthread -g
>> gdb --tui ./a.out
Komenda | Opis |
---|---|
b 5 |
Ustawia breakpoint w 5 linii |
watch x |
Obserwowanie zmian zmiennej x (debugger zatrzyma się gdy nastąpi jej modyfikacja) |
c |
Kontynuowanie debugowania |
info threads |
Informacje o wątkach |
thread 3 |
Przełączenie na wątek 3 |
n |
Następna instrukcja |
fin |
Wykonanie wszystkiego do końca bieżącej funkcji |
up |
Przejście do wyższej ramki stosu |
down |
Przejście do niższej ramki stosu |
del br |
Usunięcie wszystkich breakpointów |
CTRL + L | Odświeżenie widoku |
Mutex | Opis |
---|---|
std::mutex |
|
Funkcja | Opis |
---|---|
void lock() |
|
void unlock() |
|
bool try_lock() |
|
#include <vector>
#include <thread>
#include <chrono>
#include <iostream>
#include <mutex>
void do_work(int id)
{
this_thread::sleep_for(100ms);
m.lock();
cout << ss.rdbuf();
m.unlock();
}
int main()
{
std::mutex m;
std::vector<std::thread> threads;
for (int i = 0; i < 20; i++)
threads.emplace_back(thread(do_work, i));
for (auto && t : threads)
t.join();
return 0;
}
Pozostałe mutexy | Opis |
---|---|
std::timed_mutex |
|
std::recursive_mutex |
|
std::recursive_timed_mutex |
|
std::shared_mutex |
|
Pozostałe mutexy | Opis |
---|---|
std::lock_guard<mutex> |
|
std::unique_lock<mutex> |
|
std::scoped_lock |
|
std::shared_lock<shared_mutex> |
|
#include <vector>
#include <thread>
#include <chrono>
#include <iostream>
#include <mutex>
#include <sstream>
void do_work(int id, mutex & m)
{
this_thread::sleep_for(100ms);
stringstream ss;
ss << "Thread [" << id << "]: " << "Job done!" << std::endl;
lock_guard<mutex> lock(m);
cout << ss.rdbuf();
// Tak naprawdę nie trzeba tutaj mutexa bo to pojedyncza operacja
// Nie da się jej "zakłócić"
}
int main()
{
std::mutex m;
std::vector<std::thread> threads;
for (int i = 0; i < 20; i++)
threads.emplace_back(thread(do_work, i, std::ref(m)));
for (auto && t : threads)
t.join();
return 0;
}
#include <iostream>
#include <mutex>
#include <thread>
class X {
mutable std::mutex mtx_;
int value_ = 0;
public:
explicit X(int v) : value_(v) {}
bool operator<(const X & other) const {
std::scoped_lock l(mtx_, other.mtx_);
return value_ < other.value_;
}
};
int main() {
X x1(5);
X x2(6);
std::thread t1([&] {
if (x1 < x2)
std::cout << "x1 is less" << std::endl;
});
std::thread t2([&] {
if (x2 < x1)
std::cout << "x2 is less" << std::endl;
});
t1.join();
t2.join();
return 0;
}
#include <deque>
#include <shared_mutex>
std::deque<int> ids;
std::shared_mutex mtxIds;
int getIdsIndex() { /* ... */ }
int getIdsIndex() { /* ... */ }
int getIdsIndex() { /* ... */ }
void reader() {
int index = getIdsIndex();
std::shared_lock<std::shared_mutex> lock(mtxIds);
int value = ids[index];
lock.unlock();
process(value);
}
void writer() {
int index = getIdsIndex();
std::lock_guard<std::shared_mutex> lock(mtxIds);
// lub std::unique_lock...
ids[index] = newValue();
}
Wyżej wymienione blokady opcjonalnie przyjmują dodatkowy parametr w postaci std::defer_lock
lub std::adopt_lock
.
std::defer_lock
- Nie blokuje w momencie utworzenia tylko oczekuje na operację
std::lock()
- Stanowi to alternatywę dla
std::scope_lock
, który pojawił się dopiero w C++17
- Nie blokuje w momencie utworzenia tylko oczekuje na operację
bool operator<(const X & other) const
{
std::unique_lock<std::mutex> l1(mtx_, defer_lock);
std::unique_lock<std::mutex> l2(other.mtx_, defer_lock);
std::lock(l1, l2);
return value_ < other.value_;
}
std::adopt_lock
- Informacja dla konstruktora, że otrzyma on już zablokowane mutexy (wcześniej pojawia się std::lock()`
- Działanie przeciwne do
std::defer_lock
bool operator<(const X & other) const
{
std::lock(mtx_, other.mtx_);
std::lock_guard<std::mutex> l1(mtx_, adopt_lock);
std::lock_guard<std::mutex> l2(other.mtx_, adopt_lock);
return value_ < other.value_;
}
Funkcja std::lock()
gwarantuje zablokowanie wszystkich mutexów bez zakleszczenia niezależnie od kolejności ich pozyskania.
- Najmniejsza jednostka – 1 bajt
- Każdy bajt ma unikalny adres w pamięci
- Synchronizacja nie jest potrzebna jeśli zapisujemy coś wielowątkowo do różnych obszarów pamięci
std::vector<int> v(10, 0);
for (int = 0; i < 10; i++)
std::thread t([&]{ v[i] = i; });
- Synchronizacja jest potrzebna jeśli zapisujemy coś wielowątkowo do tych samych obszarów pamięci
std::vector<int> v;
for (int = 0; i < 10; i++)
std::thread t([&]{ v.emplace_back(i); });
- Synchronizacja jest potrzebna jeśli conajmniej jeden wątek zapisuje, a inne odczytują ten sam obszar pamięci
- Brak synchronizacji jeśli jest wymagana to wyścig/undefined behaviour
- Użycie
const
nie wymaga synchronizacji - Link do pełnego artykułu - czytaj
int a = 0;
std::mutex m;
std::thread t1([&] {
std::lock_guard<mutex> lg(m);
a = 1;
});
std::thread t2([&] {
std::lock_guard<mute> lg(m);
a = 2;
});
Lepsze rozwiązanie, które chroni przed wyścigiem (data-race) to zastosowanie zmiennej atomowej (std::atomic
). W ten sposób zapewniamy odpowiednie porządkowanie operacji dostępu do pamięci.
std::atomic<int> a = 0;
std::thread t1([&]{ a = 1; });
std::thread t2([&]{ a = 2; });
std::atomic |
Opis |
---|---|
Cechy |
|
Najważniejsze operacje |
|
// use store() / load()
#include <atomic>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
std::vector<int> generateContainer()
{
std::vector<int> input = {2, 4, 6, 8, 10, 1, 3, 5, 7, 9};
std::vector<int> output;
std::vector<std::thread> threads;
std::mutex m;
// należy jeszcze wyciągnąć lambdę przed pętlę...
for (auto i = 0u; i < input.size(); i++) {
threads.emplace_back([&, i]{
std::lock_guard<std::mutex> l(m);
// wstawianie wątków do wektora jest bezpieczne (bo jest sekwencyjne)
// należy zabezpieczyć wstawianie liczb do wektora
// bo może być problem z iteratorami
output.push_back(input[i]);
});
}
// "i" przekazujemy przez kopię
// 1 powód: jeśli "i" przekażemy przez referencję to wszystkie wątki mogą mieć
// taką samą wartość "i", np. 5
// 2 powód: część wstawiania może się odbyć dopiero przed join(), a wtedy
// zmienna "i" już nie istnieje (dangling reference)
for (auto && t : threads)
t.join();
return output;
}
std::vector<int> generateOtherContainer()
{
int start {5};
// nie trzeba std::atomic bo mamy mutex na if oraz else
std::atomic<bool> add {true};
std::vector<int> output;
std::vector<std::thread> threads;
std::mutex m;
for (int i = 0; i < 10; i++) {
threads.emplace_back([&, i]{
if (add)
{
std::lock_guard<std::mutex> l(m);
output.push_back(start+=i);
}
else
{
std::lock_guard<std::mutex> l(m);
output.push_back(start-=i);
}
add = !add;
});
}
for (auto && t : threads)
t.join();
return output;
}
void powerContainer(std::vector<int>& input)
{
std::vector<std::thread> threads;
for (auto i = 0u; i < input.size(); i++)
threads.emplace_back([&, i]{ input[i]*=input[i]; });
// nie ma potrzeby stosowania mutexu
// za każdym razem zapisujemy w inny obszar pamięci
for (auto && t : threads)
t.join();
}
void printContainer(const std::vector<int>& c)
{
for (const auto & value : c)
std::cout << value << " ";
std::cout << std::endl;
}
int main() {
auto container1 = generateContainer();
printContainer(container1);
powerContainer( // należy jeszcze wyciągnąć lambdę przed pętlę...container1);
printContainer(container1);
auto container2 = generateOtherContainer();
printContainer(container2);
powerContainer(container2);
printContainer(container2);
return 0;
}
std::memory_order
pozwala na dodatkową otymalizację i określa on typ synchronizacji. Więcej informacji - czytaj.
Typ std::memory_order |
Opis |
---|---|
memory_order_relaxed |
|
memory_order_consume |
|
memory_order_acquire |
|
memory_order_release |
|
memory_order_acq_rel |
|
memory_order_seq_cst |
|
- Aktywne czekanie (busy waiting) to stan, w którym wątek ciągle sprawdza, czy został spełniony pewien warunek
- Inna nazwa tego problemu to wirująca blokada (spinlock)
- Problem rozwiązuje zmienna warunku (condition variable)
void saveToFile(StringQueue & sq) {
ofstream file("/tmp/sth.txt");
while (file) {
while (sq.empty()) { /* nop */ }
file << sq.pop() << endl;
}
}
std::condition_variable |
Opis |
---|---|
Cechy |
|
Najważniejsze operacje |
|
- Jeśli wywołamy którąkolwiek z funkcji
notify()
, a żaden wątek nie czeka (wait()
) to "stracimy" takiego notify'a.
std::condition_variable_any |
Opis |
---|---|
Cechy |
|
Zagrożenie | Opis |
---|---|
Fałszywe przebudzenie (spurious wakeup) |
|
Utracona notyfikacja (lost wakeup) |
|
std::future
istd::promise
razem tworzą jednokierunkowy kanał komunikacji między dwoma wątkami- Wątek, który "ma coś zrobić" jako argument oprócz lambdy dostaje też
std::promise
- Wątek, który ma odebrać wynik obliczeń wywołuje
future.get()
std::promise<int> promise; // typ wyniku
std::future<int> future = promise.get_future();
// tworzymy future przez wywołanie get_future() na std::promise
// w ten sposób tworzy się kanał komunikacji
// kolejne wywolanie promise.get_future() rzuci wyjątek
auto function = [](std::promise<int> promise
{
promise.set_value(10;)
// ustawiamy promise jakąś wartość
}
std::thread t(function, std::move(promise));
// do osobnego wątku przekazujemy lambdę z promise'm
std::cout << future.get() << std::endl;
// inny wątek woła get() by wyłuskać wartość
// kolejne wywołanie get() rzuci wyjątek
t.join();
Funkcja | Opis |
---|---|
get() |
|
valid() |
|
wait() |
|
- Na
std::promise
opróczset.value()
można wywołaćset.exception()
- Wywołanie
get()
na "drugim" wątku rzuci wyjątek
promise.set_exception(std::make_exception_ptr(e));
try {
// ...
} catch (...) {
promise.set_exception(std::current_exception());
}
- Jeden wątek nadaje, ale wiele odbiera
- Obiekt
std::promise
tworzymy tylko jeden
- Obiekt
- Jedyna różnica w zastosowaniu to taka że zamiast
std::future<int> f = promise.get_future()
piszemystd::shared_future<int> f = promise.get_future().share()
- Każdy wątek powinien utworzyć swój własny obiekt
shared_future
- Każdy wątek powinien utworzyć swój własny obiekt
- Kopiowalny
- Przenoszalny
std::async
to wysokopoziomowe rozwiązanie, które automatycznie zarządza wywołaniami asynchronicznymi z podstawowymi mechanizmami synchronizacji- Przykazując funkcję do
std::assync
zwróci on obiektstd::future
za pomocą którego można się dostać do jego rezultatu (jak tylko będzie on dostępy)- Wywołując
get()
przed tym jak zostanie "obliczony" wynik, wątek poczeka na niego...
- Wywołując
- Obsługa wyjątków przez
std::promise
istd::future
- Wymaga
#include <future>
std::future<int> f = std::assync(function)
std::cout << f.get() << std::endl;
Przykład wykorzystania:
auto f2 = async(launch::async, []{
cout << "f2 started\n";
this_thread::sleep_for(1s);
Polityka | Opis |
---|---|
std::launch::async |
|
std::launch::deferred |
|
std::launch::async::deferred |
|
Bez launch policy |
|
#include <future>
#include <vector>
#include <iostream>
#include <chrono>
using namespace std;
int main()
{
auto f1 = async([] {
cout << "f1 started\n";
this_thread::sleep_for(1s);
return 42;
});
cout << "f1 spawned\n";
auto f2 = async(launch::async, []{
cout << "f2 started\n";
this_thread::sleep_for(1s);
return 2 * 42;
});
cout << "f2 spawned\n";
auto f3 = async(launch::deferred, []{
cout << "f3 started\n";
this_thread::sleep_for(1s);
return 3 * 42;
});
cout << "f3 spawned\n";
cout << "Getting f1 result\n";
auto v1 = f1.get();
cout << "Got f1 result\n";
cout << "Getting f2 result\n";
auto v2 = f2.get();
cout << "Got f2 result\n";
cout << "Getting f3 result\n";
auto v3 = f3.get();
cout << "Got f3 result\n";
vector<int> numbers = { v1, v2, v3 };
for (const auto & item : numbers)
cout << item << '\n';
return 0;
}
Wyjście:
>> f1 spawned
>> f1 started
>> f2 spawned
>> f3 spawned
>> Getting f1 result
>> f2 started
>> Got f1 result
>> Getting f2 result
>> Got f2 result
>> Getting f3 result
>> f3 started
>> Got f3 result
>> 42
>> 84
>> 126
Nie można sprawdzić w jaki sposób future (std::async
) został uruchomiony ale korzystając z wait_for()
i tego że zwraca 1 z 3 statusów:
future_status::deferred
future_status::ready
future_status::timeout
... można tak zdobyć wynik:
#include <iostream>
#include <future>
using namespace std;
void f() {
this_thread::sleep_for(1s);
}
int main() {
auto fut = async(f);
if (fut.wait_for(0s) == future_status::deferred) {
cout << "Scheduled as deffered. Calling wait() to enforce execution\n";
fut.wait();
} else {
while (fut.wait_for(100ms) != future_status::ready) {
cout << "Waiting...\n";
}
cout << "Finally...\n";
}
}
std::packaged_task
nie wykonuje się od razu (w odróżnieniu odstd::async
), to użytkownik decyduje kiedy to ma się wykonać- Przydatne jeśli chcemy do
std::async
przekazać parametry, których jeszcze nie mamy albo inny wątek/funkcja go oblicza- Wtedy wątek tworzymy dopiero jak już mamy obliczone parametry
auto globalLambda = [](int a, int b) {
std::cout << "globalLambda:\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
return std::pow(a, b);
};
// LOKALNE WYWOŁANIE
void localPackagedTask() {
std::cout << "\nlocalPackagedTask:\n";
std::packaged_task<int(int,int)> task(globalLambda);
auto result = task.get_future();
std::cout << "before task execution\n";
task(2, 9);
std::cout << "after task execution\n";
std::cout << "getting result:\t" << result.get() << '\n';
}
// WYWOŁANIE W INNYM WĄTKU
void remotePackagedTask() {
std::cout << "\nremotePackagedTask:\n";
std::packaged_task<int(int,int)> task(globalLambda);
auto result = task.get_future();
std::cout << "before task execution\n";
std::thread t(std::move(task), 2, 9);
std::cout << "after task execution\n";
t.detach(); // detach żeby było asynchronicznie (nie join())
std::cout << "getting result:\t" << result.get() << '\n';
}
// TO SAMO TYLKO KORZYSTAJĄC Z ASYNC
void remoteAsync() {
std::cout << "\nremoteAsync:\n";
auto result = std::async(std::launch::async, globalLambda, 2, 9);
std::cout << "getting result:\t" << result.get() << '\n';
}
int main() {
localPackagedTask();
remotePackagedTask();
remoteAsync();
}