Z założenia std::mutex
nie można go przenosić ani kopiować. Oznacza to, że klasa A
zawierająca muteks nie otrzyma domyślnego konstruktora przenoszenia.
Jak uczynić ten typ A
ruchomym w sposób bezpieczny dla wątków?
c++
mutex
move-constructor
Jack Sabbath
źródło
źródło
std::lock_guard
zakres 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:
Zaczniemy od konstruktora przenoszenia.
Move Constructor
Zwróć uwagę, że członek
mutex
został utworzonymutable
. Ś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 muteksumutable
.Podczas konstruowania
A
nie 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
this
first, a następnie przypisać im wartości dopiero poa.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 ostd::lock
to, 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
ReadLock
alias jest używany zamiastWriteLock
. 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
A
stanu, 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 tutajswap
: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::swap
wykonaniu zadania, blokowanie będzie miało niewłaściwą szczegółowość, blokowanie i odblokowywanie między trzema ruchami, którestd::swap
byłyby wykonywane wewnętrznie.Rzeczywiście, myślenie o tym
swap
moż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_mutex
forMutexType
.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_) {}
źródło
mutexes
do 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>();
źródło
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
const
operacje są bezpieczne dla wielu czytników (takstd
zakładają kontenery).Zastosowanie wygląda następująco:
synchronized<int> x = 7; x.read([&](auto&& v){ std::cout << v << '\n'; });
dla
int
z 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;
gostd::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
t
zostałaby zastąpiona funkcją składowąT&t()
iT const&t()const
, z wyjątkiem konstrukcji, w których musiałbyś przeskoczyć przez niektóre obręcze.Tworząc
synchronized
opakowanie zamiast części klasy, wszystko, co musimy zapewnić, to to, że klasa wewnętrznie szanuje jąconst
jako 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
synchronized
obiektach (tego samego typu) działają razem, bez konieczności programowania ich na stałe. Dodaj deklarację znajomego, a n-arnesynchronized
obiekty wielu typów mogą ze sobą współpracować. W takim przypadku być może będę musiałaccess
zrezygnować z bycia przyjacielem na linii, aby radzić sobie z konfliktami przeciążenia.przykład na żywo
źródło
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.
źródło
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.
źródło
a.mutex
jest zablokowany: tracisz ten stan. -1A a; A a2(std::move(a)); do some stuff with a
.new
podnieść instancję i umieścić ją wstd::unique_ptr
- która wydaje się czystsza i prawdopodobnie nie doprowadzi do zamieszania. Dobre pytanie.