Estou tentando responder isso sozinho, depois de passar por vários recursos online (por exemplo, este e este ), o C ++ 11 Standard, bem como as respostas fornecidas aqui.
As questões relacionadas são mescladas (por exemplo, " por que! Esperado? " É mesclado com "por que colocar compare_exchange_weak () em um loop? ") E as respostas são fornecidas de acordo.
Por que compare_exchange_weak () tem que estar em um loop em quase todos os usos?
Padrão Típico A
Você precisa obter uma atualização atômica com base no valor da variável atômica. Uma falha indica que a variável não foi atualizada com nosso valor desejado e queremos tentar novamente. Observe que não nos importamos se ele falhará devido a gravação simultânea ou falha espúria. Mas nos preocupamos que sejamos nós que fazemos essa mudança.
expected = current.load();
do desired = function(expected);
while (!current.compare_exchange_weak(expected, desired));
Um exemplo do mundo real é para vários threads adicionarem um elemento a uma lista vinculada individualmente simultaneamente. Cada thread carrega primeiro o ponteiro do cabeçalho, aloca um novo nó e anexa o cabeçalho a este novo nó. Finalmente, ele tenta trocar o novo nó com a cabeça.
Outro exemplo é implementar mutex usando std::atomic<bool>
. No máximo um segmento pode entrar na região crítica de cada vez, consoante o primeiro fio de definir current
para true
e sair do ciclo.
Padrão Típico B
Este é realmente o padrão mencionado no livro de Anthony. Ao contrário do padrão A, você deseja que a variável atômica seja atualizada uma vez, mas não se importa com quem o faz. Desde que não esteja atualizado, você tenta novamente. Isso normalmente é usado com variáveis booleanas. Por exemplo, você precisa implementar um gatilho para que uma máquina de estado siga em frente. Qual thread puxa o gatilho é independente.
expected = false;
while (!current.compare_exchange_weak(expected, true) && !expected);
Observe que geralmente não podemos usar esse padrão para implementar um mutex. Caso contrário, vários threads podem estar dentro da seção crítica ao mesmo tempo.
Dito isso, deve ser raro usar compare_exchange_weak()
fora de um loop. Ao contrário, há casos em que a versão forte está em uso. Por exemplo,
bool criticalSection_tryEnter(lock)
{
bool flag = false;
return lock.compare_exchange_strong(flag, true);
}
compare_exchange_weak
não é adequado aqui porque quando ele retorna devido a uma falha espúria, é provável que ninguém ocupe a seção crítica ainda.
Starving Thread?
Um ponto que vale a pena mencionar é que o que acontecerá se falhas espúrias continuarem a acontecer, deixando o segmento de fome? Teoricamente, isso poderia acontecer em plataformas quando compare_exchange_XXX()
é implementado como uma sequência de instruções (por exemplo, LL / SC). O acesso frequente da mesma linha de cache entre LL e SC produzirá falhas espúrias contínuas. Um exemplo mais realista é devido a uma programação burra onde todos os threads simultâneos são intercalados da seguinte maneira.
Time
| thread 1 (LL)
| thread 2 (LL)
| thread 1 (compare, SC), fails spuriously due to thread 2's LL
| thread 1 (LL)
| thread 2 (compare, SC), fails spuriously due to thread 1's LL
| thread 2 (LL)
v ..
Isso pode acontecer?
Isso não acontecerá para sempre, felizmente, graças ao que o C ++ 11 requer:
As implementações devem garantir que as operações fracas de comparação e troca não retornem consistentemente falso, a menos que o objeto atômico tenha um valor diferente do esperado ou que haja modificações simultâneas no objeto atômico.
Por que nos incomodamos em usar compare_exchange_weak () e escrever o loop nós mesmos? Podemos apenas usar compare_exchange_strong ().
Depende.
Caso 1: quando ambos precisam ser usados dentro de um loop. C ++ 11 diz:
Quando uma comparação e troca está em loop, a versão fraca renderá melhor desempenho em algumas plataformas.
No x86 (pelo menos atualmente. Talvez ele vá recorrer a um esquema semelhante ao LL / SC um dia para desempenho quando mais núcleos forem introduzidos), a versão fraca e a versão forte são essencialmente as mesmas porque ambas se resumem a uma única instrução cmpxchg
. Em algumas outras plataformas onde compare_exchange_XXX()
não é implementado atomicamente (aqui significando que nenhum primitivo de hardware existe), a versão fraca dentro do loop pode vencer a batalha porque a versão forte terá que lidar com as falhas espúrias e tentar novamente de acordo.
Mas,
raramente, podemos preferir compare_exchange_strong()
até compare_exchange_weak()
mesmo em um loop. Por exemplo, quando há muitas coisas a fazer entre o carregamento da variável atômica e a troca de um novo valor calculado (veja function()
acima). Se a própria variável atômica não muda com frequência, não precisamos repetir o cálculo caro para cada falha espúria. Em vez disso, podemos esperar compare_exchange_strong()
"absorver" tais falhas e apenas repetir o cálculo quando ele falhar devido a uma mudança de valor real.
Caso 2: Quando só compare_exchange_weak()
precisa ser usado dentro de um loop. C ++ 11 também diz:
Quando uma comparação e troca fraca exigiria um loop e um forte não, o forte é preferível.
Esse é normalmente o caso quando você faz um loop apenas para eliminar falhas espúrias da versão fraca. Você tenta novamente até que a troca seja bem-sucedida ou falhe devido à gravação simultânea.
expected = false;
while (!current.compare_exchange_weak(expected, true) && !expected);
Na melhor das hipóteses, está reinventando as rodas e tendo o mesmo desempenho que compare_exchange_strong()
. Pior? Essa abordagem falha em aproveitar todas as vantagens das máquinas que fornecem comparação e troca não espúrias em hardware .
Por último, se você fizer um loop para outras coisas (por exemplo, consulte "Padrão Típico A" acima), então há uma boa chance de que compare_exchange_strong()
também seja colocado em um loop, o que nos leva de volta ao caso anterior.