In base alla progettazione, std::mutex
non è mobile né copiabile. Ciò significa che una classe che A
possiede un mutex non riceverà un costruttore di mosse predefinito.
Come renderei A
mobile questo tipo in modo thread-safe?
In base alla progettazione, std::mutex
non è mobile né copiabile. Ciò significa che una classe che A
possiede un mutex non riceverà un costruttore di mosse predefinito.
Come renderei A
mobile questo tipo in modo thread-safe?
std::lock_guard
metodo è con ambito.
Risposte:
Cominciamo con un po 'di codice:
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:
...
Ho inserito alcuni alias di tipo piuttosto suggestivi che non trarremo vantaggio in C ++ 11, ma diventeranno molto più utili in C ++ 14. Sii paziente, ci arriveremo.
La tua domanda si riduce a:
Come scrivo il costruttore di spostamento e l'operatore di assegnazione dello spostamento per questa classe?
Inizieremo con il costruttore di mosse.
Move Constructor
Notare che il membro mutex
è stato creato mutable
. A rigor di termini questo non è necessario per i membri del trasferimento, ma presumo che tu voglia anche copiare i membri. In caso contrario, non è necessario creare il mutex mutable
.
Durante la costruzione A
, non è necessario bloccare this->mut_
. Ma devi bloccare l' mut_
oggetto da cui stai costruendo (sposta o copia). Questo può essere fatto in questo modo:
A(A&& a)
{
WriteLock rhs_lk(a.mut_);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
Notare che prima dovevamo costruire di default i membri di this
, quindi assegnare loro i valori solo dopo che a.mut_
è stato bloccato.
Sposta assegnazione
L'operatore di assegnazione dello spostamento è sostanzialmente più complicato perché non si sa se qualche altro thread accede a lhs o rhs dell'espressione di assegnazione. E in generale, è necessario proteggersi dal seguente scenario:
// Thread 1
x = std::move(y);
// Thread 2
y = std::move(x);
Ecco l'operatore di assegnazione del movimento che protegge correttamente lo scenario precedente:
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;
}
Si noti che è necessario utilizzare std::lock(m1, m2)
per bloccare i due mutex, invece di bloccarli uno dopo l'altro. Se li blocchi uno dopo l'altro, quando due thread assegnano due oggetti in ordine opposto come mostrato sopra, puoi ottenere un deadlock. Il punto std::lock
è evitare quella situazione di stallo.
Copia costruttore
Non hai chiesto dei membri della copia, ma potremmo anche parlarne ora (se non tu, qualcuno ne avrà bisogno).
A(const A& a)
{
ReadLock rhs_lk(a.mut_);
field1_ = a.field1_;
field2_ = a.field2_;
}
Il costruttore di copia assomiglia molto al costruttore di spostamento tranne per il fatto che l' ReadLock
alias viene utilizzato al posto del WriteLock
. Attualmente sono entrambi alias std::unique_lock<std::mutex>
e quindi non fa davvero alcuna differenza.
Ma in C ++ 14, avrai la possibilità di dire questo:
using MutexType = std::shared_timed_mutex;
using ReadLock = std::shared_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
Questa potrebbe essere un'ottimizzazione, ma non definitivamente. Dovrai misurare per determinare se lo è. Ma con questa modifica, è possibile copiare il costrutto dalla stessa destra in più thread contemporaneamente. La soluzione C ++ 11 ti obbliga a rendere sequenziali tali thread, anche se le rhs non vengono modificate.
Copia compito
Per completezza, ecco l'operatore di assegnazione della copia, che dovrebbe essere abbastanza autoesplicativo dopo aver letto di tutto il resto:
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;
}
E così via.
Anche gli altri membri o funzioni libere che accedono allo A
stato devono essere protetti se ci si aspetta che più thread siano in grado di chiamarli contemporaneamente. Ad esempio, ecco 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_);
}
}
Nota che se dipendi solo dal std::swap
fare il lavoro, il blocco sarà con granularità sbagliata, bloccando e sbloccando tra le tre mosse che std::swap
verrebbero eseguite internamente.
In effetti, pensare swap
può darti un'idea dell'API di cui potresti aver bisogno per fornire un "thread-safe" A
, che in generale sarà diverso da un'API "non-thread-safe", a causa del problema della "granularità del blocco".
Notare anche la necessità di proteggersi dal "self-swap". "self-swap" dovrebbe essere un no-op. Senza l'autoverifica si bloccherebbe ricorsivamente lo stesso mutex. Questo potrebbe anche essere risolto senza l'autoverifica utilizzando std::recursive_mutex
per MutexType
.
Aggiornare
Nei commenti qui sotto Yakk è piuttosto scontento di dover costruire cose predefinite nei costruttori di copia e spostamento (e ha ragione). Se ti senti abbastanza forte su questo problema, tanto da essere disposto a dedicarci memoria, puoi evitarlo in questo modo:
Aggiungi qualsiasi tipo di blocco di cui hai bisogno come membri dati. Questi membri devono precedere i dati che vengono protetti:
mutable MutexType mut_;
ReadLock read_lock_;
WriteLock write_lock_;
// ... other data members ...
E poi nei costruttori (ad esempio il costruttore di copie) fai questo:
A(const A& a)
: read_lock_(a.mut_)
, field1_(a.field1_)
, field2_(a.field2_)
{
read_lock_.unlock();
}
Oops, Yakk ha cancellato il suo commento prima che avessi la possibilità di completare questo aggiornamento. Ma merita credito per aver spinto questo problema e aver trovato una soluzione in questa risposta.
Aggiorna 2
E dyp ha avuto questo buon suggerimento:
A(const A& a)
: A(a, ReadLock(a.mut_))
{}
private:
A(const A& a, ReadLock rhs_lk)
: field1_(a.field1_)
, field2_(a.field2_)
{}
mutexes
i tipi di classe non è "l'unico vero modo". È uno strumento nella cassetta degli attrezzi e se vuoi usarlo, ecco come.
Dato che non sembra esserci un modo carino, pulito e semplice per rispondere a questa domanda: la soluzione di Anton penso sia corretta ma è decisamente discutibile, a meno che non venga fuori una risposta migliore, consiglierei di mettere una lezione del genere sul mucchio e di prendersene cura tramite un std::unique_ptr
:
auto a = std::make_unique<A>();
Ora è un tipo completamente mobile e chiunque abbia un blocco sul mutex interno mentre avviene una mossa è ancora al sicuro, anche se è discutibile se questa sia una buona cosa da fare
Se hai bisogno della semantica della copia, usa
auto a2 = std::make_shared<A>();
Questa è una risposta capovolta. Invece di incorporare "questi oggetti devono essere sincronizzati" come base del tipo, iniettalo invece in qualsiasi tipo.
Hai a che fare con un oggetto sincronizzato in modo molto diverso. Un grosso problema è che devi preoccuparti dei deadlock (blocco di più oggetti). Inoltre, in pratica non dovrebbe mai essere la tua "versione predefinita di un oggetto": gli oggetti sincronizzati sono per oggetti che saranno in conflitto e il tuo obiettivo dovrebbe essere quello di ridurre al minimo la contesa tra i thread, non di nasconderla sotto il tappeto.
Ma la sincronizzazione degli oggetti è ancora utile. Invece di ereditare da un sincronizzatore, possiamo scrivere una classe che racchiuda un tipo arbitrario nella sincronizzazione. Gli utenti devono saltare alcuni cerchi per eseguire operazioni sull'oggetto ora che è sincronizzato, ma non sono limitati a un insieme limitato di operazioni sull'oggetto codificate manualmente. Possono comporre più operazioni sull'oggetto in una o avere un'operazione su più oggetti.
Ecco un wrapper sincronizzato attorno a un tipo arbitrario 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)};
}
Funzionalità C ++ 14 e C ++ 1z incluse.
ciò presuppone che le const
operazioni siano sicure per più lettori (che è ciò che i std
contenitori assumono).
L'utilizzo si presenta come:
synchronized<int> x = 7;
x.read([&](auto&& v){
std::cout << v << '\n';
});
per un int
con accesso sincronizzato.
Lo sconsiglierei synchronized(synchronized const&)
. È raramente necessario.
Se necessario synchronized(synchronized const&)
, sarei tentato di sostituirlo T t;
con std::aligned_storage
, consentendo la costruzione del posizionamento manuale e la distruzione manuale. Ciò consente una corretta gestione della durata.
A parte ciò, potremmo copiare la fonte T
, quindi leggere da essa:
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);})
)
{}
per incarico:
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);
}
il posizionamento e le versioni di archiviazione allineate sono un po 'più complicate. La maggior parte degli accessi a t
sarebbe stata sostituita da una funzione membro T&t()
e T const&t()const
, tranne nella costruzione in cui dovresti saltare alcuni cerchi.
Creando synchronized
un wrapper invece di parte della classe, tutto ciò che dobbiamo assicurarci è che la classe rispetti internamente const
come lettore multiplo e lo scriva in un modo a thread singolo.
Nei rari casi in cui abbiamo bisogno di un'istanza sincronizzata, saltiamo attraverso i cerchi come sopra.
Mi scuso per eventuali errori di battitura in quanto sopra. Probabilmente ce ne sono alcuni.
Un vantaggio collaterale di quanto sopra è che le operazioni arbitrarie n-arie su synchronized
oggetti (dello stesso tipo) lavorano insieme, senza doverle codificare prima. Aggiungi una dichiarazione di amicizia e synchronized
oggetti diversi di più tipi potrebbero funzionare insieme. Potrei dover access
smettere di essere un amico in linea per affrontare i conflitti di sovraccarico in quel caso.
L'uso dei mutex e della semantica di spostamento C ++ è un modo eccellente per trasferire in modo sicuro ed efficiente i dati tra i thread.
Immagina un thread "produttore" che crea batch di stringhe e li fornisce a (uno o più) consumatori. Tali batch potrebbero essere rappresentati da un oggetto contenente oggetti (potenzialmente grandi) std::vector<std::string>
. Vogliamo assolutamente "spostare" lo stato interno di questi vettori nei loro consumatori senza duplicazioni inutili.
Riconosci semplicemente il mutex come parte dell'oggetto e non come parte dello stato dell'oggetto. Cioè, non vuoi spostare il mutex.
Il blocco di cui hai bisogno dipende dal tuo algoritmo o da quanto sono generalizzati i tuoi oggetti e dalla gamma di usi che permetti.
Se ti sposti solo da un oggetto "produttore" di stato condiviso a un oggetto "consumante" locale del thread, potresti essere OK per bloccare solo l' oggetto spostato da .
Se si tratta di un design più generale, dovrai bloccarli entrambi. In tal caso è necessario quindi considerare il deadlock.
Se questo è un potenziale problema, utilizzare std::lock()
per acquisire blocchi su entrambi i mutex in modo privo di deadlock.
http://en.cppreference.com/w/cpp/thread/lock
Come nota finale è necessario assicurarsi di aver compreso la semantica del movimento. Ricorda che l'oggetto spostato da viene lasciato in uno stato valido ma sconosciuto. È del tutto possibile che un thread che non esegue lo spostamento abbia un motivo valido per tentare di accedere all'oggetto spostato dall'oggetto quando potrebbe trovare quello stato valido ma sconosciuto.
Anche in questo caso il mio produttore sta solo suonando le corde e il consumatore sta portando via l'intero carico. In tal caso, ogni volta che il produttore cerca di aggiungere al vettore, potrebbe trovare il vettore non vuoto o vuoto.
In breve, se il potenziale accesso simultaneo all'oggetto spostato equivale a una scrittura, è probabile che sia OK. Se equivale a una lettura, pensa al motivo per cui va bene leggere uno stato arbitrario.
Prima di tutto, deve esserci qualcosa di sbagliato nel tuo progetto se vuoi spostare un oggetto contenente un mutex.
Ma se decidi di farlo comunque, devi creare un nuovo mutex nel costruttore di spostamenti, cioè ad esempio:
// movable
struct B{};
class A {
B b;
std::mutex m;
public:
A(A&& a)
: b(std::move(a.b))
// m is default-initialized.
{
}
};
Questo è thread-safe, perché il costruttore di spostamento può tranquillamente presumere che il suo argomento non sia utilizzato da nessun'altra parte, quindi il blocco dell'argomento non è richiesto.
A a; A a2(std::move(a)); do some stuff with a
.
new
eseguire l'istanza e di inserirla in una std::unique_ptr
, che sembra più pulita e non è probabile che porti a problemi di confusione. Buona domanda.