Ouvi dizer que isso const
significa thread-safe em C ++ 11 . Isso é verdade?
É um pouco verdade ...
Isso é o que a linguagem padrão tem a dizer sobre segurança de thread:
[1.10 / 4]
Duas avaliações de expressão entram em conflito se uma delas modifica uma localização de memória (1.7) e a outra acessa ou modifica a mesma localização de memória.
[1.10 / 21]
A execução de um programa contém uma corrida de dados se contiver duas ações conflitantes em threads diferentes, pelo menos uma das quais não é atômica e nenhuma ocorre antes da outra. Qualquer corrida de dados resulta em um comportamento indefinido.
que nada mais é do que a condição suficiente para que ocorra uma corrida de dados :
- Existem duas ou mais ações sendo realizadas ao mesmo tempo em uma determinada coisa; e
- Pelo menos um deles é uma escrita.
A Biblioteca Padrão se baseia nisso, indo um pouco mais longe:
[17.6.5.9/1]
Esta seção especifica os requisitos que as implementações devem atender para evitar data races (1.10). Cada função de biblioteca padrão deve atender a cada requisito, a menos que especificado de outra forma. As implementações podem evitar corridas de dados em casos diferentes dos especificados abaixo.
[17.6.5.9/3]
Uma função de biblioteca padrão C ++ não deve modificar direta ou indiretamente objetos (1.10) acessíveis por threads diferentes da thread atual, a menos que os objetos sejam acessados direta ou indiretamente por meio dosargumentosnão constantes da função, incluindothis
.
que, em palavras simples, diz que espera que as operações em const
objetos sejam seguras com thread . Isso significa que a Biblioteca Padrão não introduzirá uma corrida de dados, desde que as operações em const
objetos de seus próprios tipos também
- Consistem inteiramente em leituras - isto é, não há gravações--; ou
- Sincroniza gravações internamente.
Se essa expectativa não for válida para um de seus tipos, usá-lo direta ou indiretamente junto com qualquer componente da Biblioteca Padrão pode resultar em uma disputa de dados . Em conclusão, const
significa thread-safe do ponto de vista da Biblioteca Padrão . É importante notar que este é apenas um contrato e não será executado pelo compilador, se você quebrá-lo obterá um comportamento indefinido e estará por conta própria. Se const
está presente ou não, não afetará a geração de código - pelo menos não no que diz respeito a corridas de dados -.
Que isso significa const
é agora o equivalente de Java s' synchronized
?
Não . De modo nenhum...
Considere a seguinte classe excessivamente simplificada que representa um retângulo:
class rect {
int width = 0, height = 0;
public:
/*...*/
void set_size( int new_width, int new_height ) {
width = new_width;
height = new_height;
}
int area() const {
return width * height;
}
};
A função de membro area
é segura para threads ; não porque é const
, mas porque consiste inteiramente em operações de leitura. Não há gravações envolvidas e pelo menos uma gravação envolvida é necessária para que ocorra uma disputa de dados . Isso significa que você pode chamar area
de quantos threads desejar e obterá resultados corretos o tempo todo.
Observe que isso não significa que rect
seja seguro para threads . Na verdade, é fácil ver como se uma chamada para area
acontecesse ao mesmo tempo que uma chamada para set_size
em um dado rect
, então area
poderia acabar computando seu resultado com base em uma largura antiga e uma nova altura (ou mesmo em valores truncados) .
Mas está tudo bem, rect
não é const
nem esperado que seja thread-safe . Um objeto declarado const rect
, por outro lado, seria thread-safe, já que nenhuma escrita é possível (e se você está considerando const_cast
-ing algo declarado originalmente, const
então você obterá um comportamento indefinido e pronto ).
Então, o que isso significa?
Vamos supor - para fins de argumentação - que as operações de multiplicação são extremamente caras e é melhor evitá-las quando possível. Poderíamos calcular a área apenas se fosse solicitada e, em seguida, armazená-la em cache caso seja solicitada novamente no futuro:
class rect {
int width = 0, height = 0;
mutable int cached_area = 0;
mutable bool cached_area_valid = true;
public:
/*...*/
void set_size( int new_width, int new_height ) {
cached_area_valid = ( width == new_width && height == new_height );
width = new_width;
height = new_height;
}
int area() const {
if( !cached_area_valid ) {
cached_area = width;
cached_area *= height;
cached_area_valid = true;
}
return cached_area;
}
};
[Se este exemplo parecer muito artificial, você poderia substituir mentalmente int
por um número inteiro alocado dinamicamente muito grande que é inerentemente não seguro para thread e para o qual as multiplicações são extremamente caras.]
A função de membro area
não é mais segura para thread , ela está fazendo gravações agora e não está sincronizada internamente. Isso é um problema? A chamada para area
pode acontecer como parte de um construtor de cópia de outro objeto, tal construtor pode ter sido chamado por alguma operação em um contêiner padrão e, nesse ponto, a biblioteca padrão espera que essa operação se comporte como uma leitura em relação às corridas de dados . Mas estamos escrevendo!
Assim que colocarmos um rect
em um container padrão - direta ou indiretamente - estaremos firmando um contrato com a Biblioteca Padrão . Para continuar fazendo gravações em uma const
função e ainda honrando esse contrato, precisamos sincronizar internamente essas gravações:
class rect {
int width = 0, height = 0;
mutable std::mutex cache_mutex;
mutable int cached_area = 0;
mutable bool cached_area_valid = true;
public:
/*...*/
void set_size( int new_width, int new_height ) {
if( new_width != width || new_height != height )
{
std::lock_guard< std::mutex > guard( cache_mutex );
cached_area_valid = false;
}
width = new_width;
height = new_height;
}
int area() const {
std::lock_guard< std::mutex > guard( cache_mutex );
if( !cached_area_valid ) {
cached_area = width;
cached_area *= height;
cached_area_valid = true;
}
return cached_area;
}
};
Observe que tornamos a area
função thread-safe , mas ela rect
ainda não é thread-safe . Uma chamada para area
acontecer ao mesmo tempo que uma chamada para set_size
ainda pode acabar computando o valor errado, uma vez que as atribuições para width
e height
não são protegidas pelo mutex.
Se realmente quiséssemos um thread-safe rect
, usaríamos uma primitiva de sincronização para proteger o não-thread-safe rect
.
Eles estão ficando sem palavras-chave ?
Sim, eles estão. Eles estão ficando sem palavras-chave desde o primeiro dia.
Fonte : Você não sabe const
emutable
- Herb Sutter