Minha implementação de bloqueio de spin está correta e ideal?
-
21-09-2019 - |
Pergunta
Estou usando um bloqueio de spin para proteger uma seção crítica muito pequena. A contenção acontece muito Raramente, uma trava de spin é mais apropriada do que um mutex regular.
Meu código atual é o seguinte e assume x86 e gcc:
volatile int exclusion = 0;
void lock() {
while (__sync_lock_test_and_set(&exclusion, 1)) {
// Do nothing. This GCC builtin instruction
// ensures memory barrier.
}
}
void unlock() {
__sync_synchronize(); // Memory barrier.
exclusion = 0;
}
Então estou me perguntando:
- Este código está correto? Ele garante corretamente a exclusão mútua?
- Funciona em todos os sistemas operacionais X86?
- Funciona no x86_64 também? Em todos os sistemas operacionais?
- É ideal?
- Vi implementações de bloqueio de spin usando o Compare-and-Swap, mas não tenho certeza de qual é melhor.
- De acordo com a documentação do GCC Atomic Builtins (http://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/atomic-builtins.html) há também
__sync_lock_release
. Não sou especialista em barreiras de memória, então não tenho certeza se está tudo bem para eu usar isso em vez de__sync_synchronize
. - Estou otimizando para o caso em que não há disposição.
não me importo de forma alguma sobre contenção. Pode haver 1, talvez 2 outros fios tentando travar o bloqueio de spin uma vez a cada poucos dias.
Solução
Então estou me perguntando:
* Is it correct?
No contexto mencionado, eu diria que sim.
* Is it optimal?
Essa é uma pergunta carregada. Ao reinventar a roda, você também está reinventando muitos problemas que foram resolvidos por outras implementações
Eu esperaria um loop de resíduos no fracasso, onde você não está tentando acessar a palavra de bloqueio.
O uso de uma barreira completa no desbloqueio só precisa ter a semântica de liberação (é por isso que você usaria __sync_lock_release, para que você recebesse o ST1.rel no Itanium em vez de MF ou um LWSYC no PowerPC, ...). Se você realmente se preocupa apenas com x86 ou x86_64, os tipos de barreiras usadas aqui ou não não importam tanto (mas se você for onde dar o salto para a Intel's Itanium para uma porta HP-IPF, não gostaria disso).
Você não tem a instrução pause () que normalmente colocaria antes do seu loop de resíduos.
Quando há disputa que você deseja algo, semop, ou mesmo um sono idiota em desespero. Se você realmente precisa do desempenho que isso compra, a sugestão da Futex é provavelmente boa. Se você precisar do desempenho, isso compra você o suficiente para manter Este código que você tem muita pesquisa a fazer.
Observe que houve um comentário dizendo que a barreira de liberação não era necessária. Isso não é verdade mesmo no x86, porque a barreira de liberação também serve como uma instrução ao compilador para não embaralhar outros acessos de memória em torno da "barreira". Muito parecido com o que você conseguiria se usasse ASM ("" ::: "memória").
* on compare and swap
No x86, o sync_lock_test_and_set mapeará para uma instrução XCHG que possui um prefixo de bloqueio implícito. Definitivamente, o código gerado mais compacto (especialmente se você usar um byte para a "palavra de bloqueio" em vez de um int), mas não menos correto do que se você usasse o bloqueio CMPXCHG. O uso de comparar e swap pode ser usado para algorthims mais sofisticados (como colocar um ponteiro diferente de zero em metadados para o primeiro "garçom" na palavra de trava na falha).
Outras dicas
Parece bem para mim. Btw, aqui está o livro didático implementação que é mais eficiente, mesmo no caso disputado.
void lock(volatile int *exclusion)
{
while (__sync_lock_test_and_set(exclusion, 1))
while (*exclusion)
;
}
Em resposta às suas perguntas:
- Parece bom para mim
- Assumindo que o sistema operacional suporta o GCC (e o GCC tem as funções implementadas); Isso deve funcionar em todos os sistemas operacionais X86. A documentação do GCC sugere que um aviso será produzido se eles não forem suportados em uma determinada plataforma.
- Não há nada específico x86-64 aqui, então não vejo por que não. Isso pode ser expandido para cobrir algum A arquitetura que o GCC suporta, no entanto, talvez existem maneiras mais ideais de conseguir isso em arquiteturas não x86.
- Você pode estar um pouco melhor com o uso
__sync_lock_release()
nounlock()
caso; pois isso diminuirá a trava e adicionará uma barreira de memória em uma única operação. No entanto, assumindo que sua afirmação de que raramente haverá contenção; Parece bom para mim.
Se você estiver em uma versão recente do Linux, poderá usar um Futex - Um "Usuários Rápido Space Mutex":
Um bloqueio baseado em Futex corretamente programado não usará chamadas do sistema, exceto quando o bloqueio for contestado
No caso incontestado, que você está tentando otimizar com o seu spinlock, o Futex se comportará como um spinlock, sem exigir um syscall do kernel. Se a fechadura for contestada, a espera ocorre no kernel sem aguardar.
Gostaria de saber se a seguinte implementação do CAS é a correta no x86_64. É quase duas vezes mais rápido no meu laptop i7 x920 (Fedora 13 x86_64, GCC 4.4.5).
inline void lock(volatile int *locked) {
while (__sync_val_compare_and_swap(locked, 0, 1));
asm volatile("lfence" ::: "memory");
}
inline void unlock(volatile int *locked) {
*locked=0;
asm volatile("sfence" ::: "memory");
}
Não posso comentar sobre a correção, mas o título da sua pergunta levantou uma bandeira vermelha antes mesmo de ler o corpo da pergunta. Os primitivos de sincronização são diabolicamente difíceis de garantir a correção ... se possível, é melhor usar uma biblioteca bem projetada/mantida, talvez pthreads ou Boost :: Thread.
Uma melhoria é sugerida está usando Tatas (teste e teste). O uso das operações do CAS é considerado bastante caro para o processador, por isso é melhor evitá -las, se possível. Outra coisa, verifique se você não sofrerá de inversão prioritária (e se um tópico com uma alta prioridade tentar adquirir a trava, enquanto um tópico com baixa prioridade tentar libertar o bloqueio? No Windows, por exemplo, esse problema será resolvido por resolvido por resolvido por O agendador usando um impulso prioritário, mas você pode desistir explicitamente da fatia de tempo do seu tópico, caso não tenha conseguido adquirir o bloqueio nas últimas 20 tentativas (por exemplo ..)
Seu procedimento de desbloqueio não precisa da barreira da memória; A atribuição à exclusão é atômica, desde que o DWORD alinhado no x86.
No caso específico de x86 (32/64), acho que você não precisa de uma cerca de memória no código de desbloqueio. O X86 não faz nenhuma reordenação, exceto que as lojas são colocadas primeiro em um buffer de loja e, portanto, elas se tornam visíveis podem ser atrasadas para outros threads. E um thread que faz uma loja e depois lê da mesma variável lerá em seu buffer de loja se ainda não tiver sido liberado na memória. Então, tudo que você precisa é um asm
declaração para impedir reordenações de compilador. Você corre o risco de um fio que mantém a trava um pouco mais do que o necessário da perspectiva de outros threads, mas se você não se importa com a contenção, isso não deve importar. Na verdade, pthread_spin_unlock
é implementado assim no meu sistema (Linux x86_64).
Meu sistema também implementa pthread_spin_lock
usando lock decl lockvar; jne spinloop;
ao invés de usar xchg
(que é o que __sync_lock_test_and_set
usa), mas não sei se há realmente uma diferença de desempenho.
Existem algumas suposições erradas.
Primeiro, o spinlock faz sentido apenas se o Ressource estiver bloqueado em outra CPU. Se o Ressource estiver bloqueado na mesma CPU (que sempre é o caso dos sistemas uniprocessadores), você precisa relaxar o agendador para desbloquear o Ressource. Seu código atual funcionará no sistema Uniprocessor, porque o Scheduler mudará de tarefas automaticamente, mas é um desperdício de Ressource.
No sistema multiprocessador, a mesma coisa pode acontecer, mas a tarefa pode migrar de uma CPU para outra. Em suma, o uso do bloqueio de rotação está correto se você garante que suas tarefas serão executadas em diferentes CPU.
Em segundo lugar, travar um mutex é rápido (tão rápido quanto o spinlock) quando é desbloqueado. O bloqueio de mutexes (e desbloqueio) é lento (muito lento) apenas se o mutex já estiver bloqueado.
Então, no seu caso, sugiro usar mutexes.