Primeiro, você precisa aprender a pensar como um advogado de idiomas.
A especificação C ++ não faz referência a nenhum compilador, sistema operacional ou CPU específico. Faz referência a uma máquina abstrata que é uma generalização de sistemas reais. No mundo do advogado de idiomas, o trabalho do programador é escrever código para a máquina abstrata; o trabalho do compilador é atualizar esse código em uma máquina de concreto. Ao codificar rigidamente as especificações, você pode ter certeza de que seu código será compilado e executado sem modificação em qualquer sistema com um compilador C ++ compatível, seja hoje ou daqui a 50 anos.
A máquina abstrata na especificação C ++ 98 / C ++ 03 é fundamentalmente de thread único. Portanto, não é possível escrever código C ++ multiencadeado que seja "totalmente portátil" com relação às especificações. A especificação nem diz nada sobre a atomicidade de cargas e armazenamentos de memória ou a ordem em que cargas e armazenamentos podem ocorrer, independentemente de coisas como mutexes.
Obviamente, você pode escrever código multithread na prática para sistemas concretos específicos - como pthreads ou Windows. Mas não existe uma maneira padrão de escrever código multiencadeado para C ++ 98 / C ++ 03.
A máquina abstrata no C ++ 11 é multiencadeada por design. Ele também possui um modelo de memória bem definido ; isto é, diz o que o compilador pode ou não fazer quando se trata de acessar a memória.
Considere o seguinte exemplo, em que um par de variáveis globais é acessado simultaneamente por dois threads:
Global
int x, y;
Thread 1 Thread 2
x = 17; cout << y << " ";
y = 37; cout << x << endl;
O que o Thread 2 pode gerar?
No C ++ 98 / C ++ 03, esse nem é um comportamento indefinido; a pergunta em si não tem sentido porque o padrão não contempla nada chamado "fio".
No C ++ 11, o resultado é Comportamento indefinido, porque cargas e armazenamentos não precisam ser atômicos em geral. O que pode não parecer uma grande melhoria ... E por si só, não é.
Mas com o C ++ 11, você pode escrever o seguinte:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17); cout << y.load() << " ";
y.store(37); cout << x.load() << endl;
Agora as coisas ficam muito mais interessantes. Primeiro de tudo, o comportamento aqui é definido . O segmento 2 agora pode ser impresso 0 0
(se for executado antes do segmento 1), 37 17
(se for executado após o segmento 1) ou 0 17
(se for executado após o segmento 1 atribuir x, mas antes atribuir y).
O que ele não pode imprimir é 37 0
porque o modo padrão para cargas / armazenamentos atômicos no C ++ 11 é impor consistência sequencial . Isso significa apenas que todas as cargas e armazenamentos devem ser "como se" eles tivessem acontecido na ordem em que você os gravou em cada encadeamento, enquanto as operações entre encadeamentos podem ser intercaladas da maneira que o sistema desejar. Portanto, o comportamento padrão dos átomos fornece atomicidade e pedidos para cargas e armazenamentos.
Agora, em uma CPU moderna, garantir a consistência sequencial pode ser caro. Em particular, é provável que o compilador emita barreiras de memória completas entre todos os acessos aqui. Mas se o seu algoritmo puder tolerar cargas e armazenamentos fora de ordem; isto é, se requer atomicidade, mas não ordenação; ou seja, se ele pode tolerar 37 0
como saída deste programa, você pode escrever o seguinte:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17,memory_order_relaxed); cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed); cout << x.load(memory_order_relaxed) << endl;
Quanto mais moderna a CPU, maior a probabilidade de ela ser mais rápida que o exemplo anterior.
Por fim, se você precisar apenas manter cargas e armazenamentos específicos em ordem, pode escrever:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17,memory_order_release); cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release); cout << x.load(memory_order_acquire) << endl;
Isso nos leva de volta às cargas e lojas encomendadas - portanto, 37 0
não é mais uma saída possível -, mas o faz com uma sobrecarga mínima. (Neste exemplo trivial, o resultado é o mesmo que a consistência sequencial completa; em um programa maior, não seria.)
Obviamente, se as únicas saídas que você deseja ver forem 0 0
or 37 17
, você pode apenas envolver um mutex em torno do código original. Mas se você leu até aqui, aposto que já sabe como isso funciona, e essa resposta já é mais longa do que eu pretendia :-).
Então, linha de fundo. Os mutexes são ótimos e o C ++ 11 os padroniza. Mas, às vezes, por razões de desempenho, você deseja primitivas de nível inferior (por exemplo, o padrão de bloqueio clássico verificado duas vezes ). O novo padrão fornece dispositivos de alto nível, como mutexes e variáveis de condição, e também dispositivos de baixo nível, como tipos atômicos e os vários tipos de barreira à memória. Portanto, agora você pode escrever rotinas simultâneas sofisticadas e de alto desempenho, inteiramente dentro do idioma especificado pelo padrão, e pode ter certeza de que seu código será compilado e executado inalterado nos sistemas de hoje e no de amanhã.
Embora seja franco, a menos que você seja um especialista e trabalhe em algum código sério de baixo nível, provavelmente deve seguir mutexes e variáveis de condição. É isso que pretendo fazer.
Para mais informações, consulte esta postagem no blog .