Por design, std::mutex
não é móvel nem copiável. Isso significa que uma classe que A
contém um mutex não receberá um construtor de movimento padrão.
Como eu tornaria esse tipo A
móvel de maneira segura para thread?
Por design, std::mutex
não é móvel nem copiável. Isso significa que uma classe que A
contém um mutex não receberá um construtor de movimento padrão.
Como eu tornaria esse tipo A
móvel de maneira segura para thread?
std::lock_guard
método tem escopo.
Respostas:
Vamos começar com um pouco 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:
...
Coloquei alguns aliases de tipo bastante sugestivos lá, dos quais realmente não tiraremos proveito no C ++ 11, mas se tornarão muito mais úteis no C ++ 14. Seja paciente, vamos chegar lá.
Sua pergunta se resume a:
Como escrevo o construtor de movimento e o operador de atribuição de movimento para esta classe?
Começaremos com o construtor de movimento.
Construtor de movimento
Observe que o membro mutex
foi feito mutable
. Estritamente falando, isso não é necessário para os membros de movimento, mas estou assumindo que você também deseja membros de cópia. Se não for esse o caso, não há necessidade de fazer o mutex mutable
.
Ao construir A
, você não precisa travar this->mut_
. Mas você precisa bloquear o mut_
do objeto a partir do qual está construindo (mover ou copiar). Isso pode ser feito assim:
A(A&& a)
{
WriteLock rhs_lk(a.mut_);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
Observe que tivemos que construir os membros de por padrão this
primeiro e, em seguida, atribuir-lhes valores somente depois de a.mut_
ser bloqueado.
Mover Atribuição
O operador de atribuição de movimentação é substancialmente mais complicado porque você não sabe se algum outro encadeamento está acessando o lhs ou o rhs da expressão de atribuição. E, em geral, você precisa se proteger contra o seguinte cenário:
// Thread 1
x = std::move(y);
// Thread 2
y = std::move(x);
Aqui está o operador de atribuição de movimentação que protege corretamente o cenário acima:
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;
}
Observe que deve-se usar std::lock(m1, m2)
para bloquear os dois mutexes, em vez de apenas bloqueá-los um após o outro. Se você bloqueá-los um após o outro, então quando dois threads atribuem dois objetos na ordem oposta como mostrado acima, você pode obter um deadlock. O objetivo std::lock
é evitar esse impasse.
Copiar construtor
Você não perguntou sobre os membros copiadores, mas podemos também falar sobre eles agora (se não for você, alguém vai precisar deles).
A(const A& a)
{
ReadLock rhs_lk(a.mut_);
field1_ = a.field1_;
field2_ = a.field2_;
}
O construtor de cópia se parece muito com o construtor de movimento, exceto que o ReadLock
alias é usado em vez do WriteLock
. Atualmente esses dois apelidos std::unique_lock<std::mutex>
e, portanto, realmente não faz nenhuma diferença.
Mas em C ++ 14, você terá a opção de dizer o seguinte:
using MutexType = std::shared_timed_mutex;
using ReadLock = std::shared_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
Isso pode ser uma otimização, mas não definitivamente. Você terá que medir para determinar se é. Mas com essa mudança, pode-se copiar a construção do mesmo rhs em vários threads simultaneamente. A solução C ++ 11 força você a tornar tais threads sequenciais, mesmo que o rhs não esteja sendo modificado.
Copiar Atribuição
Para completar, aqui está o operador de atribuição de cópia, que deve ser bastante autoexplicativo depois de ler sobre todo o 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 etc.
Quaisquer outros membros ou funções livres que acessem A
o estado também precisarão ser protegidos se você espera que vários threads sejam capazes de chamá-los de uma vez. Por exemplo, aqui está 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_);
}
}
Observe que se você depende apenas de std::swap
fazer o trabalho, o bloqueio estará na granularidade errada, bloqueando e desbloqueando entre os três movimentos que std::swap
executariam internamente.
Na verdade, pensar sobre swap
pode fornecer uma visão sobre a API que você pode precisar fornecer para uma API "thread-safe" A
, que em geral será diferente de uma API "não thread-safe", devido ao problema de "granularidade de bloqueio".
Observe também a necessidade de proteção contra "troca automática". "self-swap" deve ser um ambiente autônomo. Sem a autoverificação, bloquearíamos recursivamente o mesmo mutex. Isso também pode ser resolvido sem a verificação automática usando std::recursive_mutex
for MutexType
.
Atualizar
Nos comentários abaixo, Yakk está muito infeliz por ter que construir coisas por padrão nos construtores de copiar e mover (e ele tem razão). Se você tiver uma opinião forte o suficiente sobre esse problema, a ponto de estar disposto a gastar memória com ele, você pode evitá-lo desta forma:
Adicione quaisquer tipos de bloqueio de que você precisa como membros de dados. Esses membros devem vir antes dos dados que estão sendo protegidos:
mutable MutexType mut_;
ReadLock read_lock_;
WriteLock write_lock_;
// ... other data members ...
E então nos construtores (por exemplo, o construtor de cópia) faça isso:
A(const A& a)
: read_lock_(a.mut_)
, field1_(a.field1_)
, field2_(a.field2_)
{
read_lock_.unlock();
}
Opa, Yakk apagou seu comentário antes que eu tivesse a chance de completar esta atualização. Mas ele merece crédito por empurrar esse problema e encontrar uma solução para essa resposta.
Atualização 2
E dyp veio com esta boa sugestão:
A(const A& a)
: A(a, ReadLock(a.mut_))
{}
private:
A(const A& a, ReadLock rhs_lk)
: field1_(a.field1_)
, field2_(a.field2_)
{}
mutexes
em tipos de classe não é o "único caminho verdadeiro". É uma ferramenta da caixa de ferramentas e se você quiser usar, é assim.
Dado que não parece ser uma maneira agradável, limpa e fácil de responder isso - a solução de Anton eu acho que é correta, mas é definitivamente discutível, a menos que uma resposta melhor apareça, eu recomendaria colocar essa classe na pilha e cuidar dela via um std::unique_ptr
:
auto a = std::make_unique<A>();
Agora é um tipo totalmente móvel e qualquer um que tenha um bloqueio no mutex interno enquanto um movimento ocorre ainda está seguro, mesmo que seja questionável se isso é uma boa coisa a fazer
Se você precisar copiar semântica, use
auto a2 = std::make_shared<A>();
Esta é uma resposta invertida. Em vez de incorporar "esses objetos precisam ser sincronizados" como uma base do tipo, injete-o sob qualquer tipo.
Você lida com um objeto sincronizado de maneira muito diferente. Um grande problema é que você precisa se preocupar com deadlocks (bloqueio de vários objetos). Basicamente, também nunca deve ser sua "versão padrão de um objeto": objetos sincronizados são para objetos que estarão em contenção e seu objetivo deve ser minimizar a contenção entre threads, não varrê-la para baixo do tapete.
Mas sincronizar objetos ainda é útil. Em vez de herdar de um sincronizador, podemos escrever uma classe que envolve um tipo arbitrário na sincronização. Os usuários precisam saltar alguns obstáculos para fazer operações no objeto agora que ele está sincronizado, mas não estão limitados a algum conjunto limitado de operações codificadas manualmente no objeto. Eles podem compor várias operações no objeto em uma ou ter uma operação em vários objetos.
Aqui está um wrapper sincronizado em torno de um tipo arbitrário 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)};
}
Recursos do C ++ 14 e C ++ 1z incluídos.
isso pressupõe que as const
operações são seguras para múltiplos leitores (que é o que os std
contêineres assumem).
O uso se parece com:
synchronized<int> x = 7;
x.read([&](auto&& v){
std::cout << v << '\n';
});
para um int
acesso sincronizado.
Eu desaconselho ter synchronized(synchronized const&)
. Raramente é necessário.
Se você precisar synchronized(synchronized const&)
, ficaria tentado a substituir T t;
por std::aligned_storage
, permitindo a construção de posicionamento manual e fazer a destruição manual. Isso permite o gerenciamento adequado da vida útil.
Exceto isso, poderíamos copiar a fonte e T
, em seguida, ler a partir dela:
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);})
)
{}
para atribuição:
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);
}
as versões de posicionamento e armazenamento alinhado são um pouco mais confusas. A maior parte do acesso a t
seria substituída por uma função de membro T&t()
e T const&t()const
, exceto na construção, onde você teria que pular alguns obstáculos.
Ao fazer synchronized
um invólucro em vez de parte da classe, tudo o que temos que garantir é que a classe seja respeitada internamente const
como sendo de múltiplos leitores e a escreva em uma única thread.
Nos raros casos em que precisamos de uma instância sincronizada, saltamos por obstáculos como o acima.
Pedimos desculpas por quaisquer erros de digitação acima. Provavelmente existem alguns.
Um benefício colateral do acima é que operações arbitrárias n-árias em synchronized
objetos (do mesmo tipo) funcionam juntas, sem ter que codificá-las previamente. Adicione uma declaração de amigo e synchronized
objetos n-ários de vários tipos podem funcionar juntos. Talvez eu tenha que deixar access
de ser um amigo inline para lidar com conflitos de sobrecarga nesse caso.
Usar mutexes e semântica de movimentação C ++ é uma maneira excelente de transferir dados entre threads de maneira segura e eficiente.
Imagine um encadeamento 'produtor' que faz lotes de strings e os fornece a (um ou mais) consumidores. Esses lotes podem ser representados por um objeto contendo objetos (potencialmente grandes) std::vector<std::string>
. Nós absolutamente queremos 'mover' o estado interno desses vetores para seus consumidores sem duplicação desnecessária.
Você simplesmente reconhece o mutex como parte do objeto, não parte do estado do objeto. Ou seja, você não deseja mover o mutex.
O bloqueio de que você precisa depende do seu algoritmo ou de quão generalizados são seus objetos e da gama de usos que você permite.
Se você apenas mover de um objeto 'produtor' de estado compartilhado para um objeto de 'consumo' local de segmento, você pode estar OK para bloquear apenas o objeto movido de .
Se for um design mais geral, você precisará bloquear ambos. Nesse caso, você precisa considerar o bloqueio morto.
Se isso for um problema potencial, use std::lock()
para adquirir bloqueios em ambos os mutexes de uma maneira livre de deadlock.
http://en.cppreference.com/w/cpp/thread/lock
Como uma nota final, você precisa ter certeza de entender a semântica do movimento. Lembre-se de que o objeto movido é deixado em um estado válido, mas desconhecido. É inteiramente possível que um encadeamento que não está realizando a movimentação tenha um motivo válido para tentar acessar o objeto movido de quando ele pode encontrar esse estado válido, mas desconhecido.
De novo, meu produtor está apenas mexendo nas cordas e o consumidor levando embora toda a carga. Nesse caso, toda vez que o produtor tenta adicionar ao vetor, ele pode encontrar o vetor não vazio ou vazio.
Em suma, se o potencial acesso simultâneo ao objeto movido for uma gravação, é provável que esteja tudo bem. Se for uma leitura, pense por que não há problema em ler um estado arbitrário.
Em primeiro lugar, deve haver algo errado com o seu design se você quiser mover um objeto que contém um mutex.
Mas se você decidir fazê-lo de qualquer maneira, terá que criar um novo mutex no construtor de movimento, que é, por exemplo:
// movable
struct B{};
class A {
B b;
std::mutex m;
public:
A(A&& a)
: b(std::move(a.b))
// m is default-initialized.
{
}
};
Isso é seguro para thread, porque o construtor de movimento pode assumir com segurança que seu argumento não é usado em nenhum outro lugar, portanto, o bloqueio do argumento não é necessário.
A a; A a2(std::move(a)); do some stuff with a
.
new
ativar a instância e colocá-la em um std::unique_ptr
- isso parece mais limpo e provavelmente não causará confusão. Boa pergunta.