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