Istnieją dwie odrębne kwestie (bezpieczeństwo wątków i wyjątki - bezpieczeństwo) i najlepiej jest rozwiązać je osobno. Aby umożliwić konstruktorom wzięcie innego obiektu jako argumentu, aby uzyskać blokadę podczas inicjowania elementów, konieczne jest w ogóle faktorowanie elementów danych w oddzielnej klasie: W ten sposób można uzyskać blokadę podczas inicjowania obiektu podrzędnego i klasy utrzymującej rzeczywiste dane może zignorować wszelkie problemy z współbieżnością. W ten sposób klasa zostanie podzielona na dwie części: class A
, aby poradzić sobie z problemami współbieżności i class A_unlocked
, aby zachować dane. Ponieważ funkcje członkowskie A_unlocked
nie mają żadnej ochrony przed współbieżnością, nie powinny być bezpośrednio widoczne w interfejsie, a zatem A_unlocked
jest prywatnym członkiem A
.
Tworzenie bezpiecznego dla wyjątków operatora przypisania przebiega prosto, wykorzystując konstruktora kopiowania. Argument jest kopiowany i członkowie są zamienione:
A_unlocked& A_unlocked::operator= (A_unlocked const& other) {
A_unlocked(other).swap(*this);
return *this;
}
Oczywiście oznacza to, że odpowiedni konstruktor kopiujący i członkiem swap()
są realizowane. Radzenie sobie z alokacją wielu zasobów, np. Wieloma obiektami przydzielonymi na stercie, jest najłatwiejsze dzięki posiadaniu odpowiedniej procedury obsługi zasobów dla każdego z obiektów. Bez użycia funkcji obsługi zasobów bardzo szybko staje się kłopotliwe, aby poprawnie usunąć wszystkie zasoby w przypadku wystąpienia wyjątku. W celu utrzymania przydzielonej pamięci sterty std::unique_ptr<T>
(lub std::auto_ptr<T>
, jeśli nie możesz używać C++ 2011) jest odpowiednim wyborem. Poniższy kod kopiuje tylko wskazane obiekty, chociaż nie ma większego sensu przydzielanie obiektów na stercie, niż przypisywanie ich członkom. W prawdziwej przykład obiekty prawdopodobnie wdrożyć metodę clone()
lub inny mechanizm do tworzenia obiektu odpowiedniego typu:
class A_unlocked {
private:
std::unique_ptr<B> pb;
std::unique_ptr<C> pc;
// ...
public:
A_unlocked(/*...*/);
A_unlocked(A_unlocked const& other);
A_unlocked& operator= (A_unlocked const& other);
void swap(A_unlocked& other);
// ...
};
A_unlocked::A_unlocked(A_unlocked const& other)
: pb(new B(*other.pb))
, pc(new C(*other.pc))
{
}
void A_unlocked::swap(A_unlocked& other) {
using std::swap;
swap(this->pb, other.pb);
swap(this->pc, other.pc);
}
Dla kawałka nitki bezpieczeństwa konieczne jest wiedzieć, że żaden inny wątek jest niepożądanym skopiowany obiekt. Aby to zrobić, użyj muteksu. Oznacza to, że class A
wygląda mniej więcej tak:
class A {
private:
mutable std::mutex d_mutex;
A_unlocked d_data;
public:
A(/*...*/);
A(A const& other);
A& operator= (A const& other);
// ...
};
uwaga, że wszyscy członkowie A
będzie trzeba zrobić pewną ochronę współbieżności jeśli obiekty typu A
mają być wykorzystywane bez blokowania zewnętrznego.Ponieważ muteks używany do ochrony przed współbieżnym dostępem nie jest tak naprawdę częścią stanu obiektu, ale musi zostać zmieniony nawet podczas odczytu stanu obiektu, jest on tworzony jako mutable
. Mając to na miejscu, tworząc konstruktor kopiujący jest prosta:
A::A(A const& other)
: d_data((std::unique_lock<std::mutex>(other.d_mutex), other.d_data)) {
}
To blokuje mutex i delegatów argument do konstruktora kopii członka. Blokada jest automatycznie zwalniana na końcu wyrażenia, niezależnie od tego, czy kopia się powiodła, czy też rzuciła wyjątek. Konstruowany obiekt nie potrzebuje żadnego blokowania, ponieważ nie ma jeszcze sposobu, aby inny wątek wiedział o tym obiekcie.
Główna logika operatora przypisania również po prostu przekazuje do bazy, używając operatora przypisania. Najtrudniejsze jest to, że istnieją dwa muteksy, które muszą być zablokowane: jeden dla przypisanego obiektu i jeden dla argumentu. Ponieważ inny wątek może przypisać dwa obiekty w dokładnie odwrotny sposób, istnieje możliwość zablokowania. Dogodnie standardowa biblioteka C++ zapewnia algorytm std::lock()
, który nabywa blokady w odpowiedni sposób, unikając ślepych blokad. Jednym ze sposobów wykorzystania tego algorytmu jest przekazanie w odblokowanych std::unique_lock<std::mutex>
przedmiotów, po jednym dla każdej mutex potrzebnego do nabycia:
A& A::operator= (A const& other) {
if (this != &other) {
std::unique_lock<std::mutex> guard_this(this->d_mutex, std::defer_lock);
std::unique_lock<std::mutex> guard_other(other.d_mutex, std::defer_lock);
std::lock(guard_this, guard_other);
*this->d_data = other.d_data;
}
return *this;
}
Jeśli w dowolnym momencie cesji jest wyjątek, strażnicy zamek wyda muteksy i moduły obsługi zasobów zwolnią wszystkie nowo przydzielone zasoby. Tak więc powyższe podejście wprowadza silną gwarancję wyjątku. Co ciekawe, przypisanie kopii musi wykonać test samoporządkowania, aby zapobiec dwukrotnemu blokowaniu tego samego muteksu. Zwykle utrzymuję, że niezbędna kontrola własna jest oznaką, że operator przypisania nie jest bezpieczny w wyjątkach, ale myślę, że powyższy kod jest wyjątkowo bezpieczny.
To poważna przeróbka odpowiedzi. Wcześniejsze wersje tej odpowiedzi były albo podatne na utratę aktualizacji, albo na awaryjną blokadę. Dzięki Yakk za wskazanie problemów. Chociaż w wyniku zajęcia się problemem wymaga więcej kodu, myślę, że każda poszczególna część kodu jest w rzeczywistości prostsza i może być zbadana pod kątem poprawności.
operatora przypisania wątku bezpieczny? te osoby przeprowadzające ankietę naprawdę ją przesuwają. – stijn
Wyjątek bezpieczny w jakim stopniu? –
Jak wdrożyć wszystko - wszystko bezpieczne? Z pewnością nie za pomocą zwykłych wskaźników C i rozmyślania z kopiami przedmiotów, które wskazują ... mamy RAI i inteligentne wskaźniki dla bezpieczeństwa, prawda? – leftaroundabout