Eu escrevi um simples programa multithreading da seguinte maneira:
static bool finished = false;
int func()
{
size_t i = 0;
while (!finished)
++i;
return i;
}
int main()
{
auto result=std::async(std::launch::async, func);
std::this_thread::sleep_for(std::chrono::seconds(1));
finished=true;
std::cout<<"result ="<<result.get();
std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}
Ele se comporta normalmente no modo de depuração no Visual studio ou -O0
no gc c e imprime o resultado após 1
segundos. Mas ficou preso e não imprime nada no modo Release ou -O1 -O2 -O3
.
c++
multithreading
thread-safety
data-race
sz ppeter
fonte
fonte
Respostas:
Dois tópicos, acessando um não-atômica, variável não vigiado são UB Isto diz respeito
finished
. Você pode fazerfinished
do tipostd::atomic<bool>
para corrigir isso.Minha correção:
Resultado:
Demonstração ao vivo no coliru
Alguém pode pensar: 'É um
bool
- provavelmente um pouco. Como isso pode ser não atômico? (Eu fiz quando comecei com o multi-threading.)Mas observe que a falta de rasgo não é a única coisa que
std::atomic
lhe dá. Também torna bem definido o acesso simultâneo de leitura e gravação de vários threads, impedindo o compilador de assumir que a leitura da variável sempre verá o mesmo valor.Criar um
bool
não-atômico e desprotegido pode causar problemas adicionais:atomic<bool>
commemory_order_relaxed
loja / carga iria trabalhar, mas ondevolatile
não. Usando volátil para isso seria UB, embora funcione na prática em implementações reais de C ++.)Para impedir que isso aconteça, o compilador deve ser informado explicitamente para não fazer isso.
Estou um pouco surpreso com a discussão em evolução sobre a possível relação
volatile
com esse problema. Assim, eu gostaria de gastar meus dois centavos:fonte
func()
e pensei: "Eu poderia otimizar isso" O otimizador não liga para threads e detecta o loop infinito, e felizmente o transformará em um "tempo (Verdadeiro)" Se olharmos para o Godbolt .org / z / Tl44iN , podemos ver isso. Se terminar,True
ele retorna. Se não, ele vai para um salto de volta incondicional de si (um loop infinito) no rótulo.L5
volatile
do C ++ 11 porque você pode obter asm idêntico aoatomic<T>
estd::memory_order_relaxed
. No entanto, ele funciona em hardware real: os caches são coerentes, portanto, uma instrução de carregamento não pode continuar lendo um valor obsoleto uma vez que uma loja em outro núcleo se compromete a armazenar em cache lá. (MESI)volatile
ainda é de UB. Você realmente nunca deve assumir que algo que é definitivo e claramente o UB é seguro apenas porque você não consegue pensar em como isso pode dar errado e funcionou quando você o tentou. Isso tem queimado as pessoas repetidas vezes.finished
com umstd::mutex
trabalho (semvolatile
ouatomic
). De fato, você pode substituir todos os átomos por um valor "simples" + esquema mutex; ainda funcionaria e seria mais lento.atomic<T>
é permitido usar um mutex interno; somenteatomic_flag
é garantido sem bloqueios.A resposta de Scheff descreve como corrigir seu código. Eu pensei em acrescentar um pouco de informação sobre o que realmente está acontecendo neste caso.
Compilei seu código no godbolt usando o nível de otimização 1 (
-O1
). Sua função compila da seguinte maneira:Então o que está acontecendo aqui? Primeiro, temos uma comparação:
cmp BYTE PTR finished[rip], 0
- isso verifica sefinished
é falso ou não.Se é não falsa (aka true) devemos sair do loop na primeira execução. Isto conseguido
jne .L4
pelo qual j umps quando n ot e qua a etiqueta.L4
em que o valor dei
(0
) é armazenada num registo para utilização posterior e a função retorna.Se for falso, no entanto, passamos para
Este é um salto incondicional, para rotular
.L5
que por acaso é o próprio comando de salto.Em outras palavras, o encadeamento é colocado em um loop ocupado infinito.
Então, por que isso aconteceu?
No que diz respeito ao otimizador, os threads estão fora de seu alcance. Ele assume que outros threads não estão lendo ou gravando variáveis simultaneamente (porque isso seria UB de corrida de dados). Você precisa dizer que ele não pode otimizar acessos ausentes. É aqui que a resposta de Scheff entra. Não vou me incomodar em repeti-lo.
Como o otimizador não é informado de que a
finished
variável pode potencialmente mudar durante a execução da função, ela vê quefinished
não é modificada pela própria função e assume que é constante.O código otimizado fornece os dois caminhos de código que resultarão da entrada na função com um valor bool constante; ou ele executa o loop infinitamente, ou o loop nunca é executado.
no
-O0
compilador (como esperado) não otimiza o corpo do loop e a comparação:portanto, a função, quando não otimizada, funciona, a falta de atomicidade aqui normalmente não é um problema, porque o código e o tipo de dados são simples. Provavelmente, o pior que podemos encontrar aqui é um valor
i
que está fora de um para o que deveria ser.Um sistema mais complexo com estruturas de dados tem muito mais probabilidade de resultar em dados corrompidos ou execução incorreta.
fonte
atomic
variáveis no código que não escreve essas variáveis. por exemplo,if (cond) foo=1;
não pode ser transformado em ASM,foo = cond ? 1 : foo;
porque essa carga + armazenamento (não um RMW atômico) pode interferir na gravação de outro encadeamento. Compiladores já estavam evitando coisas assim porque queria ser útil para escrever programas multi-threaded, mas C ++ 11 tornou oficial que compiladores tinha para não quebrar o código em que 2 threads escrevera[1]
ea[2]
Por uma questão de completude na curva de aprendizado; você deve evitar o uso de variáveis globais. Você fez um bom trabalho ao torná-lo estático, para que ele seja local na unidade de tradução.
Aqui está um exemplo:
Ao vivo na wandbox
fonte
finished
comostatic
dentro do bloco de funções. Ele ainda será inicializado apenas uma vez e, se for inicializado com uma constante, isso não exigirá bloqueio.finished
também poderiam usarstd::memory_order_relaxed
cargas e lojas mais baratas ; não há necessidade de pedido errado. outras variáveis em qualquer segmento. Mas não tenho certeza se a sugestão de @ Davislorstatic
faz sentido; se você tivesse vários threads de contagem de spin, não seria necessário pará-los com o mesmo sinalizador. Você deseja escrever a inicialização definished
uma maneira que seja compilada apenas na inicialização, mas não em um armazenamento atômico. (Como você está fazendo com afinished = false;
sintaxe C ++ 17 do inicializador padrão. Godbolt.org/z/EjoKgq ).