Comencemos con un poco de código:
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:
...
He puesto algunos alias de tipo bastante sugerentes que realmente no aprovecharemos en C ++ 11, pero que serán mucho más útiles en C ++ 14. Tenga paciencia, llegaremos allí.
Tu pregunta se reduce a:
¿Cómo escribo el constructor de movimiento y el operador de asignación de movimiento para esta clase?
Empezaremos con el constructor de movimientos.
Mover constructor
Tenga en cuenta que el miembro mutex
se ha creado mutable
. Estrictamente hablando, esto no es necesario para los miembros de movimiento, pero supongo que también quiere copiar miembros. Si ese no es el caso, no es necesario realizar el mutex mutable
.
Al construir A
, no es necesario bloquear this->mut_
. Pero necesita bloquear el mut_
objeto desde el que está construyendo (mover o copiar). Esto se puede hacer así:
A(A&& a)
{
WriteLock rhs_lk(a.mut_);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
Tenga en cuenta que tuvimos que construir por defecto los miembros de this
primero, y luego asignarles valores solo después de que a.mut_
esté bloqueado.
Mover asignación
El operador de asignación de movimiento es sustancialmente más complicado porque no sabe si algún otro hilo está accediendo a lhs o rhs de la expresión de asignación. Y, en general, debe protegerse contra el siguiente escenario:
x = std::move(y);
y = std::move(x);
Aquí está el operador de asignación de movimiento que protege correctamente el escenario anterior:
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;
}
Tenga en cuenta que se debe usar std::lock(m1, m2)
para bloquear los dos mutex, en lugar de simplemente bloquearlos uno tras otro. Si los bloquea uno tras otro, cuando dos subprocesos asignan dos objetos en orden opuesto como se muestra arriba, puede obtener un punto muerto. El punto std::lock
es evitar ese punto muerto.
Copiar constructor
No preguntaste sobre los miembros de la copia, pero también podríamos hablar de ellos ahora (si no eres tú, alguien los necesitará).
A(const A& a)
{
ReadLock rhs_lk(a.mut_);
field1_ = a.field1_;
field2_ = a.field2_;
}
El constructor de copia se parece mucho al constructor de movimiento, excepto que ReadLock
se usa el alias en lugar del WriteLock
. Actualmente estos dos alias std::unique_lock<std::mutex>
y, por lo tanto, realmente no hacen ninguna diferencia.
Pero en C ++ 14, tendrá la opción de decir esto:
using MutexType = std::shared_timed_mutex;
using ReadLock = std::shared_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
Esto puede ser una optimización, pero no definitivamente. Tendrá que medir para determinar si lo es. Pero con este cambio, se puede copiar la construcción de los mismos rhs en múltiples hilos simultáneamente. La solución C ++ 11 te obliga a hacer que dichos subprocesos sean secuenciales, aunque no se modifiquen los rhs.
Copiar asignación
Para completar, aquí está el operador de asignación de copia, que debería ser bastante autoexplicativo después de leer sobre todo lo demás:
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;
}
Y etc.
Cualquier otro miembro o función gratuita que acceda A
al estado también deberá estar protegido si espera que varios subprocesos puedan llamarlos a la vez. Por ejemplo, aquí tienes 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_);
}
}
Tenga en cuenta que si solo depende de std::swap
hacer el trabajo, el bloqueo estará en la granularidad incorrecta, bloqueando y desbloqueando entre los tres movimientos que std::swap
se realizarían internamente.
De hecho, pensar en swap
puede darte información sobre la API que podrías necesitar para proporcionar una API "segura para subprocesos" A
, que en general será diferente de una API "no segura para subprocesos", debido al problema de "granularidad de bloqueo".
También tenga en cuenta la necesidad de protegerse contra el "autointercambio". el "autointercambio" debería ser una operación prohibida. Sin la autocomprobación, se bloquearía recursivamente el mismo mutex. Esto también podría resolverse sin la autocomprobación utilizando std::recursive_mutex
for MutexType
.
Actualizar
En los comentarios a continuación, Yakk está bastante descontento por tener que construir cosas por defecto en los constructores de copia y movimiento (y tiene razón). Si se siente lo suficientemente fuerte acerca de este problema, tanto que está dispuesto a dedicar memoria a él, puede evitarlo así:
Agregue los tipos de bloqueo que necesite como miembros de datos. Estos miembros deben anteponerse a los datos que se están protegiendo:
mutable MutexType mut_;
ReadLock read_lock_;
WriteLock write_lock_;
Y luego en los constructores (por ejemplo, el constructor de copia) haga esto:
A(const A& a)
: read_lock_(a.mut_)
, field1_(a.field1_)
, field2_(a.field2_)
{
read_lock_.unlock();
}
Vaya, Yakk borró su comentario antes de que tuviera la oportunidad de completar esta actualización. Pero él merece crédito por impulsar este problema y obtener una solución a esta respuesta.
Actualización 2
Y a dyp se le ocurrió esta buena sugerencia:
A(const A& a)
: A(a, ReadLock(a.mut_))
{}
private:
A(const A& a, ReadLock rhs_lk)
: field1_(a.field1_)
, field2_(a.field2_)
{}
std::lock_guard
método is tiene alcance.