As implementações intertravadas baseadas no CompareExchange devem usar o SpinWait?

8

Abaixo está uma implementação de um método intertravado baseado em Interlocked.CompareExchange.

É aconselhável que esse código use uma SpinWaitrotação antes de reiterar?

public static bool AddIfLessThan(ref int location, int value, int comparison)
{
    int currentValue;
    do
    {
        currentValue = location; // Read the current value
        if (currentValue >= comparison) return false; // If "less than comparison" is NOT satisfied, return false
    }
    // Set to currentValue+value, iff still on currentValue; reiterate if not assigned
    while (Interlocked.CompareExchange(ref location, currentValue + value, currentValue) != currentValue);
    return true; // Assigned, so return true
}

Eu já vi SpinWaitusado nesse cenário, mas minha teoria é que deveria ser desnecessário. Afinal, o loop contém apenas algumas instruções e sempre há um thread em progresso.

Digamos que dois threads estejam correndo para executar esse método, e o primeiro thread seja bem-sucedido imediatamente, enquanto o segundo thread inicialmente não faz alterações e precisa reiterar. Sem outros concorrentes, é possível que o segundo thread falhe em sua segunda tentativa ?

Se o segundo thread do exemplo não puder falhar na segunda tentativa, o que podemos ganhar com a SpinWait? Eliminando alguns ciclos no caso improvável de centenas de threads correrem para executar o método?

Timo
fonte
3
O @MindSwipe OP não está perguntando se é necessário usar o Interlocked; ele deveria usar o SpinWait em vez de apenas o corpo do loop vazio?
Matthew Watson
1
Provavelmente, você deve usar apenas SpinOncepara impedir que um sistema operacional de thread único passe fome. Veja stackoverflow.com/questions/37799381/…
Matthew Watson
1
@MindSwipe A condição de corrida já foi tratada corretamente devido ao uso Interlocked. Para esclarecimento, estou interessado apenas em saber se um SpinWaité significativo ou não , por exemplo, para salvar significativamente os ciclos da CPU ou (obrigado @MatthewWatson!) Impedir que um sistema operacional de thread único passe fome.
Timo
1
@AloisKraus Entendo isso em teoria, mas meu raciocínio é que esse loop será bem - sucedido na próxima tentativa , independentemente de esperarmos ou não. É por isso que espero que SpinWaitisso não reduza o consumo de energia, pois o mesmo número de tentativas será realizado de qualquer maneira! (Com dois segmentos, que é uma tentativa para o primeiro e dois para o segundo segmento.)
Timo
1
@Timo, acho que você pode se beneficiar de um bloqueio de rotação em plataformas nas quais o Interlocked não é suportado por hardware, mas deve ser emulado.
Nick

Respostas:

2

Minha opinião de não especialista é que, nesse caso em particular, onde duas threads ocasionalmente chamam AddIfLessThan, a SpinWaité desnecessário. Pode ser benéfico se os dois threads estiverem chamando AddIfLessThanem um loop apertado, para que cada thread possa progredir ininterruptamente por alguns μs.

Na verdade, fiz um experimento e medi o desempenho de um thread chamando AddIfLessThanem um loop apertado versus dois threads. Os dois threads precisam quase quatro vezes mais para criar o mesmo número de loops (cumulativamente). A adição de SpinWaita à mistura torna os dois segmentos apenas um pouco mais lentos que o único.

Theodor Zoulias
fonte
1
Que efeito o SpinWait tem com um thread, por exemplo, existe um motivo para não usá-lo?
Ian Ringrose
2
@IanRingrose torna 15% mais lento. Declaro uma variável do tipo SpinWaitdentro do AddIfLessThanmétodo e, embora seja um tipo de valor e seu SpinOncemétodo nunca seja chamado, ele ainda adiciona alguma sobrecarga.
Theodor Zoulias 04/11/19
3
@TheodorZoulias: Você pode declarar no seu corpo IL para pular o init dos habitantes locais, o que deve devolver o seu desempenho. Você precisa emitir o código IL para isso porque ainda não há suporte ao idioma. Consulte github.com/dotnet/csharplang/blob/master/proposals/… Quando você quiser girar uma vez, pode chamar Reset nele e depois SpinOnce.
Alois Kraus
1
Obrigado @TheodorZoulias! Para sua informação, os dois tópicos foram apenas um exemplo para ilustrar meu argumento. Esta é uma biblioteca de classes e pode ser usada como o consumidor achar melhor. O melhor que podemos fazer é fazer suposições sobre a probabilidade de condições de corrida com muitos tópicos. Dado o pouco tempo que a operação leva, considero improvável que muitos encadeamentos executem essa operação se sobrepondo.
Timo
1
Se você estiver atualizando um loop restrito, provavelmente considere tornar o valor privado e atualizar uma vez no final do loop. Se o valor compartilhado precisar ser mantido mais ou menos real e o loop for extremamente longo, considere atualizar o valor à medida que você avança uma barra de progresso, uma vez a cada X ms.
curiousguy
2

Dois tópicos simplesmente não são um assunto para SpinWaitdiscussão. Mas esse código não nos diz quantos threads realmente podem competir pelo recurso e, com um número relativamente alto de threads, o uso do SpinWaitpode se tornar benéfico. Em particular, com um número maior de encadeamentos, a fila virtual de encadeamentos, que está tentando adquirir o recurso com sucesso, fica mais longa e os encadeamentos que são servidos no final têm boas chances de exceder seu intervalo de tempo alocado pelo agendador que, por sua vez, pode levar a um maior consumo de CPU e pode afetar a execução de outros encadeamentos agendados, mesmo com maior prioridade. oSpinWaittem uma boa resposta para essa situação, definindo um limite superior de rotações permitidas, após o qual a alternância de contexto será executada. Portanto, é uma troca razoável entre a necessidade de fazer uma chamada cara do sistema, a fim de acionar uma alternância de contexto e um consumo descontrolado de CPU do modo de usuário que corre o risco de afetar a execução de outros threads em determinadas situações.

Dmytro Mukalov
fonte
Os dois tópicos foram apenas um exemplo para ilustrar meu argumento. Esta é uma biblioteca de classes. Deveríamos ser capazes de lidar com qualquer cenário realista, levando em consideração sua probabilidade. Dado o pouco tempo que a operação leva, considero improvável que muitos encadeamentos executem essa operação se sobrepondo.
Timo
Você faz um argumento justo. Concordo que, com muitos threads, alguns threads falhariam muitos antes de serem bem-sucedidos. No entanto, parte de mim pensa que a operação é tão pequena que, digamos, realizá-la 100 vezes ainda é amendoim. Com isso em mente, você esperaria que isso SpinWaitcausasse um exagero ou não?
Timo
2
@Timo, a única resposta é a medição, especialmente considerando que este será um código de biblioteca que deve ser capaz de lidar com vários cenários de carga e hardware que eu presumo. Para maior justiça e, portanto, para melhor rendimento, use a SpinWaitestratégia é melhor (com custo relativamente pequeno de dezenas de microssegundos) do que não usá-lo, a menos que você exclua antecipadamente a possibilidade de cenários de alta carga baixa, portanto, encontre um ponto de carga no qual o rendimento possa ser afetado forneceria uma dica, se você precisa ou não (observe que isso também pode depender do número de núcleos).
Dmytro Mukalov 05/11/19
Com tantos encadeamentos com necessidade de um recurso comum por tanto tempo tão frequentemente que um consumirá seu intervalo de tempo, eu consideraria fazer cópias particulares do recurso (que serão sincronizadas posteriormente) ou mesmo abandonar os encadeamentos.
curiousguy
@curiousguy Conforme afirmado em 4 comentários, esta é uma biblioteca de classes. Obviamente, o código de consumo pode usar uma abordagem específica de caso. O objetivo da biblioteca de classes, no entanto, é ser resiliente a vários casos de uso. É por isso que permanece a pergunta em que situações a SpinWaitagregação de valor. Até agora, esses parecem ser (A) o caso de núcleo único e (B) possivelmente o caso de contenção muito alta.
Timo