Ouvi dizer que i ++ não é uma instrução thread-safe, pois na montagem ela se reduz a armazenar o valor original como uma temporária em algum lugar, incrementando-o e substituindo-o, o que pode ser interrompido por uma troca de contexto.
No entanto, estou pensando sobre ++ i. Pelo que posso dizer, isso se reduziria a uma única instrução de montagem, como 'add r1, r1, 1' e, como é apenas uma instrução, não seria interrompida por uma troca de contexto.
Alguém pode esclarecer? Estou assumindo que uma plataforma x86 está sendo usada.
c++
c
multithreading
Samoz
fonte
fonte
Respostas:
Você ouviu errado. Pode muito bem ser que
"i++"
seja thread-safe para um compilador específico e uma arquitetura de processador específica, mas não é obrigatório nos padrões. Na verdade, como o multi-threading não faz parte dos padrões ISO C ou C ++ (a) , você não pode considerar nada como thread-safe com base no que você acha que será compilado.É bastante viável que
++i
possa compilar em uma sequência arbitrária, como:que não seria thread-safe em minha CPU (imaginária) que não tem instruções de incremento de memória. Ou pode ser inteligente e compilá-lo em:
onde
lock
desabilita eunlock
habilita interrupções. Mas, mesmo assim, isso pode não ser seguro para thread em uma arquitetura que tem mais de uma dessas CPUs compartilhando memória (olock
pode desabilitar interrupções para apenas uma CPU).A linguagem em si (ou bibliotecas para ela, se não estiver embutida na linguagem) fornecerá construções thread-safe e você deve usá-las ao invés de depender de seu entendimento (ou possivelmente mal-entendido) de qual código de máquina será gerado.
Coisas como Java
synchronized
epthread_mutex_lock()
(disponível para C / C ++ em alguns sistemas operacionais) são o que você precisa examinar (a) .(a) Esta pergunta foi feita antes que os padrões C11 e C ++ 11 fossem concluídos. Essas iterações agora introduziram suporte de threading nas especificações da linguagem, incluindo tipos de dados atômicos (embora eles, e os threads em geral, sejam opcionais, pelo menos em C).
fonte
Você não pode fazer uma declaração geral sobre ++ i ou i ++. Por quê? Considere incrementar um número inteiro de 64 bits em um sistema de 32 bits. A menos que a máquina subjacente tenha uma instrução quad word "carregar, incrementar, armazenar", o incremento desse valor exigirá várias instruções, qualquer uma das quais pode ser interrompida por uma troca de contexto de thread.
Além disso,
++i
nem sempre é "adicionar um ao valor". Em uma linguagem como C, incrementar um ponteiro, na verdade, adiciona o tamanho da coisa apontada. Ou seja, sei
for um ponteiro para uma estrutura de++i
32 bytes , adiciona 32 bytes. Enquanto quase todas as plataformas têm uma instrução de "valor de incremento no endereço de memória" que é atômica, nem todas têm uma instrução atômica "adicione valor arbitrário ao valor no endereço de memória".fonte
Ambos são inseguros para thread.
Uma CPU não pode fazer matemática diretamente com a memória. Ele faz isso indiretamente, carregando o valor da memória e fazendo as contas com os registros da CPU.
i ++
++ i
Para ambos os casos, há uma condição de corrida que resulta no valor i imprevisível.
Por exemplo, vamos supor que haja dois threads ++ i simultâneos com cada um usando o registro a1, b1 respectivamente. E, com a troca de contexto executada da seguinte forma:
Como resultado, i não se torna i + 2, ele se torna i + 1, o que é incorreto.
Para remediar isso, as CPUs modernas fornecem algum tipo de instruções de CPU LOCK, UNLOCK durante o intervalo em que a troca de contexto é desabilitada.
No Win32, use InterlockedIncrement () para fazer i ++ para segurança de thread. É muito mais rápido do que confiar no mutex.
fonte
Se você estiver compartilhando até mesmo um int entre threads em um ambiente com vários núcleos, precisará de barreiras de memória adequadas. Isso pode significar o uso de instruções interligadas (consulte InterlockedIncrement no win32, por exemplo) ou o uso de uma linguagem (ou compilador) que oferece certas garantias de thread-safe. Com o reordenamento de instruções no nível da CPU, caches e outros problemas, a menos que você tenha essas garantias, não presuma que nada compartilhado entre threads é seguro.
Editar: uma coisa que você pode presumir com a maioria das arquiteturas é que, se estiver lidando com palavras únicas alinhadas corretamente, você não terminará com uma única palavra contendo uma combinação de dois valores que foram misturados. Se duas gravações acontecerem uma sobre a outra, uma vencerá e a outra será descartada. Se você for cuidadoso, pode tirar vantagem disso e ver que ++ i ou i ++ são thread-safe na situação de gravador único / leitor múltiplo.
fonte
Se você deseja um incremento atômico em C ++, pode usar as bibliotecas C ++ 0x (o
std::atomic
tipo de dados) ou algo como TBB.Houve uma época em que as diretrizes de codificação GNU diziam que atualizar tipos de dados que cabiam em uma palavra era "geralmente seguro", mas esse conselho é errado para máquinas SMP,
errado para algumas arquiteturase errado ao usar um compilador de otimização.Para esclarecer o comentário "atualizando o tipo de dados de uma palavra":
É possível que duas CPUs em uma máquina SMP gravem no mesmo local de memória no mesmo ciclo e, em seguida, tentem propagar a alteração para as outras CPUs e o cache. Mesmo se apenas uma palavra de dados estiver sendo gravada, de forma que as gravações levem apenas um ciclo para serem concluídas, elas também acontecem simultaneamente, então você não pode garantir qual gravação será bem-sucedida. Você não obterá dados parcialmente atualizados, mas uma gravação desaparecerá porque não há outra maneira de lidar com este caso.
Compare-and-swap coordena corretamente entre múltiplas CPUs, mas não há razão para acreditar que cada atribuição de variável de tipos de dados de uma palavra usará compare-and-swap.
E embora um compilador de otimização não afete como um carregamento / armazenamento é compilado, ele pode mudar quando o carregamento / armazenamento acontece, causando sérios problemas se você espera que suas leituras e gravações aconteçam na mesma ordem em que aparecem no código-fonte ( o mais famoso sendo o bloqueio com verificação dupla não funciona no vanilla C ++).
NOTA Minha resposta original também disse que a arquitetura Intel de 64 bits foi quebrada ao lidar com dados de 64 bits. Isso não é verdade, então editei a resposta, mas minha edição alegou que os chips PowerPC estavam quebrados. Isso é verdade ao ler valores imediatos (ou seja, constantes) em registradores (consulte as duas seções denominadas "Ponteiros de carregamento" nas listas 2 e 4). Mas há uma instrução para carregar dados da memória em um ciclo (
lmw
), então removi essa parte da minha resposta.fonte
Em x86 / Windows em C / C ++, você não deve presumir que é seguro para threads. Você deve usar InterlockedIncrement () e InterlockedDecrement () se precisar de operações atômicas.
fonte
Se sua linguagem de programação não diz nada sobre threads, mas roda em uma plataforma multithread, como pode qualquer construção de linguagem ser thread-safe?
Como outros apontaram: você precisa proteger qualquer acesso multithread a variáveis por chamadas específicas da plataforma.
Existem bibliotecas por aí que abstraem a especificidade da plataforma, e o próximo padrão C ++ adaptou seu modelo de memória para lidar com threads (e, portanto, pode garantir a segurança das threads).
fonte
Mesmo se for reduzido a uma única instrução de montagem, incrementando o valor diretamente na memória, ainda não é seguro para threads.
Ao incrementar um valor na memória, o hardware realiza uma operação de "leitura-modificação-gravação": ele lê o valor da memória, o incrementa e o grava de volta na memória. O hardware x86 não tem como incrementar diretamente na memória; a RAM (e os caches) só podem ler e armazenar valores, não modificá-los.
Agora suponha que você tenha dois núcleos separados, em soquetes separados ou compartilhando um único soquete (com ou sem um cache compartilhado). O primeiro processador lê o valor e, antes que possa escrever de volta o valor atualizado, o segundo processador o lê. Depois que os dois processadores escreverem o valor de volta, ele terá sido incrementado apenas uma vez, não duas vezes.
Existe uma maneira de evitar esse problema; Os processadores x86 (e a maioria dos processadores multi-core que você encontrará) são capazes de detectar esse tipo de conflito no hardware e sequenciá-lo, de forma que toda a sequência de leitura-modificação-gravação pareça atômica. No entanto, como isso é muito caro, só é feito quando solicitado pelo código, em x86 geralmente por meio do
LOCK
prefixo. Outras arquiteturas podem fazer isso de outras maneiras, com resultados semelhantes; por exemplo, load-linked / store-condicional e atômico compare-and-swap (processadores x86 recentes também têm este último).Observe que usar
volatile
não ajuda aqui; ele apenas informa ao compilador que a variável pode ter sido modificada externamente e a leitura dessa variável não deve ser armazenada em cache em um registro ou otimizada. Não faz com que o compilador use primitivas atômicas.A melhor maneira é usar primitivas atômicas (se seu compilador ou bibliotecas as tiverem) ou fazer o incremento diretamente na montagem (usando as instruções atômicas corretas).
fonte
Nunca presuma que um incremento será compilado para uma operação atômica. Use InterlockedIncrement ou quaisquer funções semelhantes existentes em sua plataforma de destino.
Edit: Acabei de pesquisar esta questão específica e o incremento no X86 é atômico em sistemas de processador único, mas não em sistemas de multiprocessador. Usar o prefixo de bloqueio pode torná-lo atômico, mas é muito mais portátil apenas para usar InterlockedIncrement.
fonte
De acordo com esta lição de montagem em x86, você pode adicionar atomicamente um registro a um local de memória , portanto, potencialmente seu código pode executar atomicamente '++ i' ou 'i ++'. Mas como disse em outro post, o C ansi não aplica atomicidade à operação '++', então você não pode ter certeza do que seu compilador irá gerar.
fonte
O padrão C ++ de 1998 não tem nada a dizer sobre threads, embora o próximo padrão (lançado neste ano ou no próximo) sim. Portanto, você não pode dizer nada inteligente sobre thread-safety das operações sem se referir à implementação. Não é apenas o processador que está sendo usado, mas a combinação do compilador, do sistema operacional e do modelo de thread.
Na ausência de documentação em contrário, eu não presumiria que qualquer ação seja segura para thread, particularmente com processadores multi-core (ou sistemas multi-processador). Eu também não confiaria nos testes, já que os problemas de sincronização de thread provavelmente surgirão apenas por acidente.
Nada é seguro para thread, a menos que você tenha uma documentação que diga que é para o sistema específico que você está usando.
fonte
Jogue i no armazenamento local do thread; não é atômico, mas então não importa.
fonte
AFAIK, de acordo com o padrão C ++, a leitura / gravação em um
int
é atômica.No entanto, tudo o que isso faz é se livrar do comportamento indefinido que está associado a uma corrida de dados.
Mas ainda haverá uma corrida de dados se os dois threads tentarem incrementar
i
.Imagine o seguinte cenário:
Deixe
i = 0
inicialmente:O thread A lê o valor da memória e o armazena em seu próprio cache. O segmento A incrementa o valor em 1.
O thread B lê o valor da memória e o armazena em seu próprio cache. A linha B aumenta o valor em 1.
Se tudo isso fosse um único tópico, você obteria
i = 2
na memória.Mas com ambos os threads, cada thread grava suas alterações e, portanto, o Thread A grava de
i = 1
volta na memória e o Thread B escrevei = 1
na memória.Está bem definido, não há destruição parcial ou construção ou qualquer tipo de arrancamento de um objeto, mas ainda é uma corrida de dados.
Para incrementar atomicamente,
i
você pode usar:std::atomic<int>::fetch_add(1, std::memory_order_relaxed)
A ordenação relaxada pode ser usada porque não nos importamos onde essa operação ocorre, tudo o que importa é que a operação de incremento seja atômica.
fonte
Você diz "é apenas uma instrução, seria ininterrupta por uma troca de contexto." - isso é muito bom para uma única CPU, mas e uma CPU dual core? Então você pode realmente ter dois threads acessando a mesma variável ao mesmo tempo, sem nenhuma troca de contexto.
Sem saber o idioma, a resposta é testá-lo até o fim.
fonte
Eu acho que se a expressão "i ++" é a única em uma instrução, é equivalente a "++ i", o compilador é inteligente o suficiente para não manter um valor temporal, etc. Então, se você pode usá-los alternadamente (caso contrário, você ganhou pergunte qual usar), não importa o que você usar, pois eles são quase os mesmos (exceto pela estética).
De qualquer forma, mesmo que o operador de incremento seja atômico, isso não garante que o resto do cálculo será consistente se você não usar os bloqueios corretos.
Se você quiser experimentar por si mesmo, escreva um programa em que N threads incrementem simultaneamente uma variável compartilhada M vezes cada ... se o valor for menor que N * M, algum incremento foi sobrescrito. Experimente com pré-incremento e pós-incremento e diga-nos ;-)
fonte
Para um contador, eu recomendo usar o idioma compare e swap, que não é bloqueado e é seguro para thread.
Aqui está em Java:
fonte