Const significa thread-safe em C ++ 11?

115

Ouvi dizer que isso constsignifica thread-safe em C ++ 11 . Isso é verdade?

Que isso significa consté agora o equivalente de Java s' synchronized?

Eles estão ficando sem palavras-chave ?

K-ballo
fonte
1
O C ++-faq é geralmente administrado pela comunidade C ++, e você poderia gentilmente nos pedir opiniões em nosso chat.
Cachorro
@DeadMG: Eu não conhecia o C ++ - faq e sua etiqueta, foi sugerido em um comentário.
K-ballo
2
Onde você ouviu que const significa thread-safe?
Mark B
2
@Mark B: Herb Sutter e Bjarne Stroustrup estavam dizendo isso na Standard C ++ Foundation , veja o link na parte inferior da resposta.
K-ballo
NOTA PARA OS QUE VÊM AQUI: a verdadeira questão NÃO é se const significa thread-safe. Isso seria um absurdo, pois caso contrário ele iria dizer que você deve ser capaz de ir em frente e marcar todos os métodos thread-safe como const. Em vez disso, a pergunta que estamos realmente fazendo é const IMPLIES thread-safe, e é sobre isso que trata esta discussão.
user541686

Respostas:

131

Ouvi dizer que isso constsignifica 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 :

  1. Existem duas ou mais ações sendo realizadas ao mesmo tempo em uma determinada coisa; e
  2. 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 constobjetos sejam seguras com thread . Isso significa que a Biblioteca Padrão não introduzirá uma corrida de dados, desde que as operações em constobjetos de seus próprios tipos também

  1. Consistem inteiramente em leituras - isto é, não há gravações--; ou
  2. 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, constsignifica 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 constestá 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 areade quantos threads desejar e obterá resultados corretos o tempo todo.

Observe que isso não significa que rectseja seguro para threads . Na verdade, é fácil ver como se uma chamada para areaacontecesse ao mesmo tempo que uma chamada para set_sizeem um dado rect, então areapoderia acabar computando seu resultado com base em uma largura antiga e uma nova altura (ou mesmo em valores truncados) .

Mas está tudo bem, rectnão é constnem 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, constentã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 intpor 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 areapode 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 rectem um container padrão - direta ou indiretamente - estaremos firmando um contrato com a Biblioteca Padrão . Para continuar fazendo gravações em uma constfunçã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 areafunção thread-safe , mas ela rectainda não é thread-safe . Uma chamada para areaacontecer ao mesmo tempo que uma chamada para set_sizeainda pode acabar computando o valor errado, uma vez que as atribuições para widthe heightnã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 constemutable - Herb Sutter

K-ballo
fonte
6
@Ben Voigt: É meu entendimento que a especificação do C ++ 11 para std::stringestá redigida de uma forma que já proíbe o COW . Não me lembro dos detalhes ...
K-ballo
3
@BenVoigt: Não. Isso simplesmente evitaria que tais coisas não fossem sincronizadas - ou seja, não seguras para thread. C ++ 11 já bane COW explicitamente - esta passagem em particular não tem nada a ver com isso, entretanto, e não baniria COW.
Cachorro
2
Parece-me que existe uma lacuna lógica. [17.6.5.9/3] proíbe "demais", dizendo "não deve modificar direta ou indiretamente"; deve dizer "não deve, direta ou indiretamente, introduzir uma corrida de dados", a menos que uma gravação atômica esteja em algum lugar definida para não ser uma "modificação". Mas não consigo encontrar isso em lugar nenhum.
Andy Prowl
1
Provavelmente deixei todo o meu ponto um pouco mais claro aqui: isocpp.org/blog/2012/12/… Obrigado por tentar ajudar de qualquer maneira.
Andy Prowl
1
às vezes me pergunto quem foi o único (ou os diretamente envolvidos) realmente responsável por escrever alguns parágrafos padrão como esses.
pepper_chico