Z założenia std::mutexnie można go przenosić ani kopiować. Oznacza to, że klasa Azawierająca muteks nie otrzyma domyślnego konstruktora przenoszenia.
Jak uczynić ten typ Aruchomym w sposób bezpieczny dla wątków?
Z założenia std::mutexnie można go przenosić ani kopiować. Oznacza to, że klasa Azawierająca muteks nie otrzyma domyślnego konstruktora przenoszenia.
Jak uczynić ten typ Aruchomym w sposób bezpieczny dla wątków?
std::lock_guardzakres metody is.
Odpowiedzi:
Zacznijmy od fragmentu kodu:
class A
{
using MutexType = std::mutex;
using ReadLock = std::unique_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
mutable MutexType mut_;
std::string field1_;
std::string field2_;
public:
...
Umieściłem tam kilka dość sugestywnych aliasów typów, których tak naprawdę nie wykorzystamy w C ++ 11, ale staną się znacznie bardziej przydatne w C ++ 14. Cierpliwości, dotrzemy na miejsce.
Twoje pytanie sprowadza się do:
Jak napisać konstruktor przenoszenia i operator przypisania przenoszenia dla tej klasy?
Zaczniemy od konstruktora przenoszenia.
Move Constructor
Zwróć uwagę, że członek mutexzostał utworzony mutable. Ściśle mówiąc, nie jest to konieczne dla członków ruchu, ale zakładam, że chcesz również kopiować członków. Jeśli tak nie jest, nie ma potrzeby tworzenia muteksu mutable.
Podczas konstruowania Anie musisz blokować this->mut_. Ale musisz zablokować mut_obiekt, z którego konstruujesz (przenieść lub skopiować). Można to zrobić w następujący sposób:
A(A&& a)
{
WriteLock rhs_lk(a.mut_);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
Zauważ, że musieliśmy domyślnie skonstruować elementy składowe thisfirst, a następnie przypisać im wartości dopiero po a.mut_zablokowaniu.
Przenieś przypisanie
Operator przypisania przeniesienia jest znacznie bardziej skomplikowany, ponieważ nie wiadomo, czy inny wątek uzyskuje dostęp do lewego lub prawego skrzydła wyrażenia przypisania. Ogólnie rzecz biorąc, musisz wystrzegać się następującego scenariusza:
// Thread 1
x = std::move(y);
// Thread 2
y = std::move(x);
Oto operator przypisania ruchu, który prawidłowo chroni powyższy scenariusz:
A& operator=(A&& a)
{
if (this != &a)
{
WriteLock lhs_lk(mut_, std::defer_lock);
WriteLock rhs_lk(a.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
return *this;
}
Zauważ, że należy użyć std::lock(m1, m2)do zablokowania dwóch muteksów, zamiast po prostu blokować je jeden po drugim. Jeśli zablokujesz je jeden po drugim, to gdy dwa wątki przypiszą dwa obiekty w odwrotnej kolejności, jak pokazano powyżej, możesz uzyskać zakleszczenie. Chodzi o std::lockto, aby uniknąć tego impasu.
Copy Constructor
Nie pytałeś o członków kopii, ale równie dobrze możemy porozmawiać o nich teraz (jeśli nie ty, ktoś będzie ich potrzebował).
A(const A& a)
{
ReadLock rhs_lk(a.mut_);
field1_ = a.field1_;
field2_ = a.field2_;
}
Konstruktor kopiujący wygląda podobnie do konstruktora przenoszenia, z wyjątkiem tego, że ReadLockalias jest używany zamiast WriteLock. Obecnie oba pseudonimy, std::unique_lock<std::mutex>więc nie ma to większego znaczenia.
Ale w C ++ 14 będziesz mieć możliwość powiedzenia tego:
using MutexType = std::shared_timed_mutex;
using ReadLock = std::shared_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
Może to być optymalizacja, ale nie zdecydowanie. Będziesz musiał zmierzyć, aby określić, czy tak jest. Ale dzięki tej zmianie można kopiować konstrukcję z tego samego prawa w wielu wątkach jednocześnie. Rozwiązanie C ++ 11 zmusza cię do tworzenia takich wątków sekwencyjnie, mimo że prawa oś nie są modyfikowane.
Kopiuj przypisanie
Dla kompletności, oto operator przypisania kopiowania, który powinien być dość oczywisty po przeczytaniu wszystkiego innego:
A& operator=(const A& a)
{
if (this != &a)
{
WriteLock lhs_lk(mut_, std::defer_lock);
ReadLock rhs_lk(a.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
field1_ = a.field1_;
field2_ = a.field2_;
}
return *this;
}
Itd.
Wszyscy inni członkowie lub bezpłatne funkcje, które mają dostęp do Astanu, również będą musiały być chronione, jeśli oczekujesz, że wiele wątków będzie mogło wywoływać je jednocześnie. Na przykład tutaj swap:
friend void swap(A& x, A& y)
{
if (&x != &y)
{
WriteLock lhs_lk(x.mut_, std::defer_lock);
WriteLock rhs_lk(y.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
using std::swap;
swap(x.field1_, y.field1_);
swap(x.field2_, y.field2_);
}
}
Zwróć uwagę, że jeśli polegasz tylko na std::swapwykonaniu zadania, blokowanie będzie miało niewłaściwą szczegółowość, blokowanie i odblokowywanie między trzema ruchami, które std::swapbyłyby wykonywane wewnętrznie.
Rzeczywiście, myślenie o tym swapmoże dać ci wgląd w API, które możesz potrzebować, aby zapewnić "bezpieczne wątkowo" A, które ogólnie będzie różniło się od "niegwintowanego" API, ze względu na problem "granularności blokowania".
Zwróć również uwagę na potrzebę ochrony przed „samodzielną wymianą”. „Self-swap” nie powinno być opcją. Bez samokontroli można by rekurencyjnie zablokować ten sam muteks. Można to również rozwiązać bez samokontroli, używając std::recursive_mutexfor MutexType.
Aktualizacja
W komentarzach poniżej Yakk jest dość niezadowolony z konieczności domyślnego konstruowania rzeczy w konstruktorach kopiowania i przenoszenia (i ma rację). Jeśli czujesz się wystarczająco mocno w tej kwestii, tak bardzo, że chcesz poświęcić jej pamięć, możesz tego uniknąć w następujący sposób:
Dodaj dowolne typy blokad, których potrzebujesz jako członków danych. Członkowie ci muszą poprzedzić chronione dane:
mutable MutexType mut_;
ReadLock read_lock_;
WriteLock write_lock_;
// ... other data members ...
A potem w konstruktorach (np. Konstruktorze kopiującym) zrób tak:
A(const A& a)
: read_lock_(a.mut_)
, field1_(a.field1_)
, field2_(a.field2_)
{
read_lock_.unlock();
}
Ups, Yakk usunął swój komentarz, zanim udało mi się ukończyć tę aktualizację. Ale on zasługuje na uznanie za forsowanie tej kwestii i znalezienie rozwiązania w tej odpowiedzi.
Zaktualizuj 2
I dyp wpadł na tę dobrą sugestię:
A(const A& a)
: A(a, ReadLock(a.mut_))
{}
private:
A(const A& a, ReadLock rhs_lk)
: field1_(a.field1_)
, field2_(a.field2_)
{}
mutexesdo typów klas nie jest „jedyną prawdziwą drogą”. Jest to narzędzie w przyborniku i jeśli chcesz go użyć, oto jak.
Biorąc pod uwagę, że nie ma ładnego, czystego, łatwego sposobu na odpowiedź - rozwiązanie Antona uważam za poprawne, ale jest zdecydowanie dyskusyjne, chyba że pojawi się lepsza odpowiedź, poleciłbym postawić taką klasę na stosie i dbać o nią przez std::unique_ptr:
auto a = std::make_unique<A>();
Jest to teraz w pełni ruchomy typ i każdy, kto ma blokadę wewnętrznego muteksu podczas wykonywania ruchu, jest nadal bezpieczny, nawet jeśli można dyskutować, czy jest to dobra rzecz do zrobienia
Jeśli potrzebujesz semantyki kopiowania, po prostu użyj
auto a2 = std::make_shared<A>();
To jest odwrócona odpowiedź. Zamiast osadzać „te obiekty muszą być zsynchronizowane” jako podstawę typu, zamiast tego wstrzyknij go pod dowolny typ.
Zsynchronizowanym obiektem postępujesz w bardzo różny sposób. Jednym dużym problemem jest to, że musisz się martwić o zakleszczenia (blokowanie wielu obiektów). W zasadzie nigdy nie powinno to być twoją „domyślną wersją obiektu”: zsynchronizowane obiekty są przeznaczone dla obiektów, które będą ze sobą konkurować, a Twoim celem powinno być zminimalizowanie konfliktu między wątkami, a nie zamiatanie ich pod dywan.
Ale synchronizacja obiektów jest nadal przydatna. Zamiast dziedziczyć po synchronizatorze, możemy napisać klasę, która zawija dowolny typ w synchronizacji. Użytkownicy muszą przeskoczyć kilka obręczy, aby wykonać operacje na obiekcie teraz, gdy jest on zsynchronizowany, ale nie są ograniczeni do jakiegoś ręcznie zakodowanego ograniczonego zestawu operacji na obiekcie. Mogą łączyć wiele operacji na obiekcie w jedną lub wykonywać operacje na wielu obiektach.
Oto zsynchronizowana otoka wokół dowolnego typu T:
template<class T>
struct synchronized {
template<class F>
auto read(F&& f) const&->std::result_of_t<F(T const&)> {
return access(std::forward<F>(f), *this);
}
template<class F>
auto read(F&& f) &&->std::result_of_t<F(T&&)> {
return access(std::forward<F>(f), std::move(*this));
}
template<class F>
auto write(F&& f)->std::result_of_t<F(T&)> {
return access(std::forward<F>(f), *this);
}
// uses `const` ness of Syncs to determine access:
template<class F, class... Syncs>
friend auto access( F&& f, Syncs&&... syncs )->
std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
{
return access2( std::index_sequence_for<Syncs...>{}, std::forward<F>(f), std::forward<Syncs>(syncs)... );
};
synchronized(synchronized const& o):t(o.read([](T const&o){return o;})){}
synchronized(synchronized && o):t(std::move(o).read([](T&&o){return std::move(o);})){}
// special member functions:
synchronized( T & o ):t(o) {}
synchronized( T const& o ):t(o) {}
synchronized( T && o ):t(std::move(o)) {}
synchronized( T const&& o ):t(std::move(o)) {}
synchronized& operator=(T const& o) {
write([&](T& t){
t=o;
});
return *this;
}
synchronized& operator=(T && o) {
write([&](T& t){
t=std::move(o);
});
return *this;
}
private:
template<class X, class S>
static auto smart_lock(S const& s) {
return std::shared_lock< std::shared_timed_mutex >(s.m, X{});
}
template<class X, class S>
static auto smart_lock(S& s) {
return std::unique_lock< std::shared_timed_mutex >(s.m, X{});
}
template<class L>
static void lock(L& lockable) {
lockable.lock();
}
template<class...Ls>
static void lock(Ls&... lockable) {
std::lock( lockable... );
}
template<size_t...Is, class F, class...Syncs>
friend auto access2( std::index_sequence<Is...>, F&&f, Syncs&&...syncs)->
std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
{
auto locks = std::make_tuple( smart_lock<std::defer_lock_t>(syncs)... );
lock( std::get<Is>(locks)... );
return std::forward<F>(f)(std::forward<Syncs>(syncs).t ...);
}
mutable std::shared_timed_mutex m;
T t;
};
template<class T>
synchronized< T > sync( T&& t ) {
return {std::forward<T>(t)};
}
Zawiera funkcje C ++ 14 i C ++ 1z.
zakłada to, że constoperacje są bezpieczne dla wielu czytników (tak stdzakładają kontenery).
Zastosowanie wygląda następująco:
synchronized<int> x = 7;
x.read([&](auto&& v){
std::cout << v << '\n';
});
dla intz dostępem zsynchronizowanym.
Radziłbym nie mieć synchronized(synchronized const&). Rzadko jest potrzebny.
Jeśli zajdzie taka potrzeba synchronized(synchronized const&), kusiłbym, aby zastąpić T t;go std::aligned_storage, pozwalając na ręczne umieszczanie konstrukcji i ręczne niszczenie. To pozwala na właściwe zarządzanie przez całe życie.
Poza tym moglibyśmy skopiować źródło T, a następnie przeczytać z niego:
synchronized(synchronized const& o):
t(o.read(
[](T const&o){return o;})
)
{}
synchronized(synchronized && o):
t(std::move(o).read(
[](T&&o){return std::move(o);})
)
{}
do cesji:
synchronized& operator=(synchronized const& o) {
access([](T& lhs, T const& rhs){
lhs = rhs;
}, *this, o);
return *this;
}
synchronized& operator=(synchronized && o) {
access([](T& lhs, T&& rhs){
lhs = std::move(rhs);
}, *this, std::move(o));
return *this;
}
friend void swap(synchronized& lhs, synchronized& rhs) {
access([](T& lhs, T& rhs){
using std::swap;
swap(lhs, rhs);
}, *this, o);
}
rozmieszczenie i wyrównane wersje przechowywania są nieco bardziej chaotyczne. Większość dostępu do programu tzostałaby zastąpiona funkcją składową T&t()i T const&t()const, z wyjątkiem konstrukcji, w których musiałbyś przeskoczyć przez niektóre obręcze.
Tworząc synchronizedopakowanie zamiast części klasy, wszystko, co musimy zapewnić, to to, że klasa wewnętrznie szanuje ją constjako wielokrotnego czytnika i zapisuje ją w sposób jednowątkowy.
W rzadkich przypadkach potrzebujemy zsynchronizowanej instancji, przeskakujemy przez obręcze, takie jak powyżej.
Przepraszamy za wszelkie literówki wymienione powyżej. Prawdopodobnie jest kilka.
Dodatkową korzyścią wynikającą z powyższego jest to, że n-dowolne dowolne operacje na synchronizedobiektach (tego samego typu) działają razem, bez konieczności programowania ich na stałe. Dodaj deklarację znajomego, a n-arne synchronizedobiekty wielu typów mogą ze sobą współpracować. W takim przypadku być może będę musiał accesszrezygnować z bycia przyjacielem na linii, aby radzić sobie z konfliktami przeciążenia.
Używanie muteksów i semantyki przenoszenia w języku C ++ to doskonały sposób na bezpieczne i wydajne przesyłanie danych między wątkami.
Wyobraź sobie wątek „producenta”, który tworzy partie sznurków i dostarcza je (jednemu lub większej liczbie) konsumentów. Te partie mogą być reprezentowane przez obiekt zawierający (potencjalnie duże) std::vector<std::string>obiekty. Absolutnie chcemy „przenieść” stan wewnętrzny tych wektorów na ich konsumentów bez niepotrzebnego powielania.
Po prostu rozpoznajesz muteks jako część obiektu, a nie część stanu obiektu. Oznacza to, że nie chcesz przenosić muteksu.
Rodzaj blokowania, którego potrzebujesz, zależy od algorytmu lub stopnia uogólnienia obiektów i zakresu zastosowań, na które zezwalasz.
Jeśli przenosisz się tylko z obiektu „producenta” stanu współużytkowanego do obiektu „konsumującego” lokalnego wątku, możesz zablokować tylko obiekt przeniesiony z obiektu.
Jeśli jest to bardziej ogólny projekt, musisz zablokować oba. W takim przypadku należy rozważyć zamknięcie martwe.
Jeśli jest to potencjalny problem, użyj, std::lock()aby uzyskać blokady na obu muteksach w sposób wolny od zakleszczenia.
http://en.cppreference.com/w/cpp/thread/lock
Na koniec upewnij się, że rozumiesz semantykę przenoszenia. Przypomnij sobie, że przeniesiony z obiektu pozostaje w prawidłowym, ale nieznanym stanie. Jest całkowicie możliwe, że wątek, który nie wykonuje przenoszenia, ma ważny powód, aby próbować uzyskać dostęp do przeniesionego obiektu, gdy może znaleźć ten prawidłowy, ale nieznany stan.
Znowu mój producent po prostu wystukuje struny, a konsument zabiera cały ładunek. W takim przypadku za każdym razem, gdy producent próbuje dodać do wektora, może się okazać, że wektor jest niepusty lub pusty.
Krótko mówiąc, jeśli potencjalny równoczesny dostęp do przeniesionego z obiektu sprowadza się do zapisu, prawdopodobnie będzie OK. Jeśli sprowadza się to do czytania, zastanów się, dlaczego czytanie dowolnego stanu jest w porządku.
Przede wszystkim musi być coś nie tak z projektem, jeśli chcesz przenieść obiekt zawierający mutex.
Ale jeśli i tak zdecydujesz się to zrobić, musisz stworzyć nowy mutex w konstruktorze przenoszenia, czyli np .:
// movable
struct B{};
class A {
B b;
std::mutex m;
public:
A(A&& a)
: b(std::move(a.b))
// m is default-initialized.
{
}
};
Jest to bezpieczne dla wątków, ponieważ konstruktor przenoszenia może bezpiecznie założyć, że jego argument nie jest używany nigdzie indziej, więc blokowanie argumentu nie jest wymagane.
A a; A a2(std::move(a)); do some stuff with a.
newpodnieść instancję i umieścić ją w std::unique_ptr- która wydaje się czystsza i prawdopodobnie nie doprowadzi do zamieszania. Dobre pytanie.