Wstęp
Biblioteka standardowa C++11 wprowadza klasę std::lock_guard typu RAII implementującą idiom muteksu (blokuje muteks w konstruktorze i zwalnia go w destruktorze). Stosując muteksy należy przyswoić sobie dwie ważne zasady:- Nie wolno zwracać lub zapisywać wskaźników lub referencji poza zasięgiem lock-a (blokady).
- Należy blokować zasoby tylko przez minimalny okres czasu, potrzebny do wykonania danej operacji.
#include <iostream> #include <string> #include <thread> #include <mutex> using namespace std; class Storage { private: std::mutex interanl_mutex; std::string buff; public: void increment(const std::string id) { std::lock_guard<std::mutex> guard(interanl_mutex); buff += id; } void show() { std::lock_guard<std::mutex> guard(interanl_mutex); std::cout << buff << endl; } }; void update(const std::string id, int loop_counter, Storage& storage) { for (int i = 0; i < loop_counter; ++i) { std::this_thread::sleep_for(std::chrono::seconds(1)); storage.increment(id); } } int main() { Storage s; std::thread t1{update, std::string("a"), 2, std::ref(s)}; std::thread t2{update, std::string("b"), 3, std::ref(s)}; t1.join(); t2.join(); s.show(); return 0; }Wynik:
ababb
Deadlock - zakleszczenie
Z samym zagadnieniem można zapoznać się tutaj:Kilka zasad dzięki, którym można uniknąć tego rodzaju problemów:
- Nie wolno czekać na inny wątek jeżeli istnieje szansa, że to on może czekać na nas.
- Nie wolno zakładać blokad, jeżeli jakaś została już założona. Jeżeli wątek potrzebuje kilku muteksów należy wykonać blokowanie jako pojedynczą operację (stosując np. std::lock)
- Jeżeli tworzona jest biblioteka, nie należy wołać kodu użytkownika trzymając jednocześnie blokadę na muteksach. Nigdy nie można być pewnym, czy kod użytkownika też nie założy swojej własnej blokady.
- Jeżeli nie można skorzystać z std::lock do założenia kilku blokad jednocześnie, należy pamiętać by blokady zakładać zawsze w tej samej kolejności.
- Nie wolno zakładać blokad, jeżeli kod posiada już blokadę na muteksa z niższego poziomu (można stworzyć hierarchical_mutex, który nie jest częścią standardu ale jest łatwy w implementacji [1])
- http://en.cppreference.com/w/cpp/thread/unique_lock
- http://en.cppreference.com/w/cpp/thread/lock_guard/lock_guard
- std::adopt_lock jest używany by zaznaczyć, że muteks został już zablokowany i std::lock_guard albo std::unique_lock powinien tylko zaadoptować własność, zamiast próbować go jeszcze raz blokować.
- std::defer_lock występuje tylko dla std::unique_lock i mówi o tym że muteks nie powinien zostać zablokowany przez konstruktor. Można tą operację wykonać później wołając np. funkcję std::lock.
std::mutex mutex1; std::mutex mutex2; std::lock(mutex1, mutex2); std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock); std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock);Alternatywa z wykorzystaniem std::unique_lock:
std::mutex mutex1; std::mutex mutex2; std::unique_lock<std::mutex> lock1(mutex1, std::defer_lock); std::unique_lock<std::mutex> lock2(mutex2, std::defer_lock); std::lock(lock1, lock2);
Inicjowanie zasobów współdzielonych przez wątki
Często zachodzi potrzeba zainicjowania zmiennej tylko przez jeden z wątków. Zamiast samodzielnie tworzyć kod blokujący zasób i sprawdzający czy został on zainicjowany, biblioteka standardowa wprowadza specjalny mechanizm w postaci std::call_flag i std::call_once, które pozwalają na wykonanie takiej operacji (inicjowania) dokładnie raz w bezpieczny sposób.W C++11 istnieje konieczność stosowania std::ref w przypadku argumentów do funkcji inicjującej, inaczej kompilator zaprotestuje. Zostało to naprawione w C++17.
#include <iostream> #include <string> #include <vector> #include <thread> #include <mutex> using namespace std; std::mutex vec_mutex; std::once_flag vec_flag; void show(std::string description, std::vector<int>& vec) { std::cout << description; for(const auto& v : vec) std::cout << v << " "; std::cout << std::endl; } void init(std::vector<int>& vec) { for(auto& v : vec) v = 0; show("Initialize (call_once): ", vec); } void update(std::vector<int>& vec, int index) { // std::call_once(vec_flag, init, std::ref(vec)); // Działa w C++11 std::call_once(vec_flag, init, vec); // Działa w C++17 std::lock_guard<std::mutex> guard(vec_mutex); vec[index] += 1; } int main() { std::vector<int> vec = {9, 9, 9}; show("Creation (in main thread): ", vec); std::thread t1{update, std::ref(vec), 0}; std::thread t2{update, std::ref(vec), 2}; t1.join(); t2.join(); show("All thread finish: ", vec); return 0; }Wynik:
Creation (in main thread): 9 9 9 Initialize (call_once): 0 0 0 All thread finish: 1 0 1
Bibliografia
- [1] Anthony Williams: C++ Concurency in Action. USA Manning publications Co., 2012. Rozdział 3, str. 33.
Brak komentarzy:
Prześlij komentarz