Estou usando Cygwin GCC e executo este código:
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
unsigned u = 0;
void foo()
{
u++;
}
int main()
{
vector<thread> threads;
for(int i = 0; i < 1000; i++) {
threads.push_back (thread (foo));
}
for (auto& t : threads) t.join();
cout << u << endl;
return 0;
}
Compilado com a linha: g++ -Wall -fexceptions -g -std=c++14 -c main.cpp -o main.o
.
Ele imprime 1000, o que é correto. No entanto, eu esperava um número menor devido a threads substituindo um valor incrementado anteriormente. Por que este código não sofre de acesso mútuo?
Minha máquina de teste tem 4 núcleos e não coloquei restrições no programa que conheço.
O problema persiste ao substituir o conteúdo do compartilhado foo
por algo mais complexo, por exemplo
if (u % 3 == 0) {
u += 4;
} else {
u -= 1;
}
c++
race-condition
mafu
fonte
fonte
u
volta na memória. A CPU realmente fará coisas incríveis como notar que a linha de memóriau
não está no cache da CPU e reiniciará a operação de incremento. É por isso que ir de x86 para outras arquiteturas pode ser uma experiência reveladora!while true; do res=$(./a.out); if [[ $res != 1000 ]]; then echo $res; break; fi; done;
imprime 999 ou 998 em meu sistema.Respostas:
foo()
é tão curto que cada thread provavelmente termina antes mesmo de o próximo ser gerado. Se você adicionar um sleep por um tempo aleatóriofoo()
antes deu++
, poderá começar a ver o que espera.fonte
É importante entender que uma condição de corrida não garante que o código será executado incorretamente, apenas que ele pode fazer qualquer coisa, pois é um comportamento indefinido. Incluindo a execução conforme o esperado.
Particularmente em máquinas X86 e AMD64, as condições de corrida em alguns casos raramente causam problemas, já que muitas das instruções são atômicas e as garantias de coerência são muito altas. Essas garantias são um tanto reduzidas em sistemas multiprocessadores onde o prefixo de bloqueio é necessário para que muitas instruções sejam atômicas.
Se em sua máquina o incremento for uma operação atômica, provavelmente será executado corretamente, embora de acordo com o padrão da linguagem seja um comportamento indefinido.
Especificamente, espero que neste caso o código possa estar sendo compilado para uma instrução atômica Fetch and Add (ADD ou XADD no assembly X86) que é de fato atômica em sistemas de processador único, no entanto, em sistemas de multiprocessador não é garantido que seja atômico e um bloqueio seria necessário para torná-lo assim. Se você estiver executando em um sistema multiprocessador, haverá uma janela onde os threads podem interferir e produzir resultados incorretos.
Especificamente, eu compilei seu código para montagem usando https://godbolt.org/ e
foo()
compilar para:Isso significa que ele está apenas executando uma instrução add que, para um único processador, será atômica (embora, como mencionado acima, não seja para um sistema com vários processadores).
fonte
inc [u]
não é atômico. OLOCK
prefixo é necessário para tornar uma instrução verdadeiramente atômica. O OP está simplesmente dando sorte. Lembre-se de que mesmo que você esteja dizendo à CPU "adicione 1 à palavra neste endereço", a CPU ainda precisa buscar, incrementar, armazenar esse valor e outra CPU pode fazer a mesma coisa simultaneamente, fazendo com que o resultado seja incorreto.Acho que não é tanto a coisa se você colocar um sono antes ou depois do
u++
. Em vez disso, a operação seu++
traduz em código que é - em comparação com a sobrecarga de threads de geração que chamamfoo
- executado muito rapidamente de modo que é improvável que seja interceptado. No entanto, se você "prolongar" a operaçãou++
, a condição de corrida se tornará muito mais provável:resultado:
694
BTW: eu também tentei
e isso me deu na maioria das vezes
1997
, mas às vezes1995
.fonte
else u -= 1
ser executado? Mesmo em um ambiente paralelo, o valor nunca deveria não caber%2
, não é?else u -= 1
é executado uma vez, na primeira vez que foo () é chamado, quando u == 0. Os 999 vezes restantes u são ímpares eu += 2
são executados resultando em u = -1 + 999 * 2 = 1997; ou seja, a saída correta. Uma condição de corrida às vezes faz com que um dos + = 2 seja sobrescrito por um thread paralelo e você obtém 1995.Ele sofre de uma condição de corrida. Coloque
usleep(1000);
antesu++;
emfoo
e eu ver uma saída diferente (<1000) a cada vez.fonte
A provável resposta de por que a condição de corrida não se manifestou para você, embora não existir, é que
foo()
é tão rápido, em comparação com o tempo que leva para iniciar uma discussão, que cada segmento termina antes do próximo pode até mesmo começar. Mas...Mesmo com sua versão original, o resultado varia de acordo com o sistema: tentei do seu jeito em um Macbook (quad-core) e, em dez execuções, consegui 1000 três vezes, 999 seis vezes e 998 uma vez. Portanto, a corrida é um tanto rara, mas claramente presente.
Você compilou com
'-g'
, que tem uma maneira de fazer os bugs desaparecerem. Recompilei seu código, ainda inalterado, mas sem o'-g'
, e a corrida ficou muito mais acentuada: consegui 1000 uma vez, 999 três vezes, 998 duas vezes, 997 duas vezes, 996 uma vez e 992 uma vez.Ré. a sugestão de adicionar um sono - isso ajuda, mas (a) um tempo fixo de sono deixa os fios ainda distorcidos pelo tempo de início (sujeito à resolução do temporizador), e (b) um sono aleatório os espalha quando o que queremos é aproxime-os. Em vez disso, eu os codificaria para aguardar um sinal de início, para que pudesse criá-los todos antes de deixá-los trabalhar. Com esta versão (com ou sem
'-g'
), obtenho resultados em todos os lugares, tão baixos quanto 974 e não superiores a 998:fonte
-g
bandeira não "faz com que os bugs desapareçam" de forma alguma. O-g
sinalizador nos compiladores GNU e Clang simplesmente adiciona símbolos de depuração ao binário compilado. Isso permite que você execute ferramentas de diagnóstico como GDB e Memcheck em seus programas com alguma saída legível por humanos. Por exemplo, quando o Memcheck é executado em um programa com vazamento de memória, ele não informa o número da linha, a menos que o programa tenha sido criado com o-g
sinalizador.-O2
vez de-g
". Mas, dito isso, se você nunca teve a alegria de caçar um bug que só se manifestaria quando compilado sem ele-g
, considere-se um sortudo. Isso pode acontecer, com alguns dos mais sutil dos bugs de aliasing. Eu já vi isso, embora não recentemente, e eu podia acreditar que talvez fosse um capricho de um compilador proprietária de idade, então eu vou acreditar em você, provisoriamente, sobre as versões modernas do GNU e Clang.-g
não o impede de usar otimizações. por exemplo,gcc -O3 -g
faz o mesmo quegcc -O3
, mas com metadados de depuração. O gdb dirá "otimizado" se você tentar imprimir algumas variáveis.-g
poderia talvez mudar as localizações relativas de algumas coisas na memória, se alguma das coisas que ele adiciona fizer parte da.text
seção. Definitivamente, ocupa espaço no arquivo de objeto, mas acho que depois de vincular tudo acaba em uma extremidade do segmento de texto (não na seção), ou não faz parte de um segmento. Talvez possa afetar onde as coisas são mapeadas para bibliotecas dinâmicas.