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.
Przykład poniżej pokazuje wykorzystanie
std::lock_guard.
#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])
W tym miejscu należy wspomnieć o nowej klasie
std::unique_lock, którą można stosować tam gdzie należy odroczyć blokowanie muteksów, albo gdy istnieje potrzeba przetransferowania własności do innego obiektu. Ma ona podobny konstruktor do
std::lock_guard.
Drugi parametr konstruktora może przyjmować kilka interesujących parametrów:
- 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.
Dwa przykłady jak może wyglądać zakładanie kilku blokad jednocześnie:
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.