Articles

Spinlock vs Semaphore

Um spinlock é um mecanismo de bloqueio, e portanto um mecanismo de exclusão mútua (estritamente 1 para 1). Funciona consultando repetidamente e/ou modificando um local de memória, geralmente de forma atómica. Isto significa que adquirir um spinlock é uma operação “ocupada” que possivelmente queima ciclos de CPU por um longo tempo (talvez para sempre!) enquanto efetivamente não alcança “nada”.
O principal incentivo para tal abordagem é o fato de que um interruptor de contexto tem uma sobrecarga equivalente a girar algumas centenas (ou talvez milhares) de vezes, então se uma trava pode ser adquirida queimando alguns ciclos girando, isto pode muito bem ser muito mais eficiente. Além disso, para aplicações em tempo real pode não ser aceitável bloquear e esperar que o programador volte para elas em algum momento distante no futuro.

#include <boost/atomic.hpp>
class spinlock {
private:
typedef enum {Locked, Unlocked} LockState;
boost::atomic<LockState> state_;
public:
spinlock() : state_(Unlocked) {}
void lock()
{
while (state_.exchange(Locked, boost::memory_order_acquire) == Locked) {
/* busy-wait */
}
}
void unlock()
{
state_.store(Unlocked, boost::memory_order_release);
}
};// Usagespinlock s;
s.lock();
// access data structure here
s.unlock();

Um semáforo, ao contrário, ou não gira, ou gira apenas por um tempo muito curto (como uma otimização para evitar a sobrecarga do syscall). Se um semáforo não puder ser adquirido, ele bloqueia, dando tempo de CPU para uma thread diferente que esteja pronta para rodar. Isto pode, naturalmente, significar que alguns milissegundos passam antes da sua thread ser agendada novamente, mas se isto não for problema (normalmente não é) então pode ser uma abordagem muito eficiente, conservadora da CPU.

Um sistema bem projetado normalmente tem pouco ou nenhum congestionamento (isto significa que nem todas as threads tentam adquirir a trava exatamente ao mesmo tempo). Por exemplo, normalmente não se escreve código que adquire uma fechadura, depois carrega meio megabyte de dados compactados com zip da rede, decodifica e analisa os dados, e finalmente modifica uma referência compartilhada (anexar dados a um container, etc.) antes de liberar a fechadura. Em vez disso, a fechadura seria adquirida apenas com o propósito de acessar o recurso compartilhado.
Desde que isso significa que há consideravelmente mais trabalho fora da seção crítica do que dentro dela, naturalmente a probabilidade de um thread estar dentro da seção crítica é relativamente baixa, e assim poucos threads estão lutando pela fechadura ao mesmo tempo. Claro que de vez em quando dois threads vão tentar adquirir a fechadura ao mesmo tempo (se isso não pudesse acontecer você não precisaria de uma fechadura!), mas isso é mais a exceção do que a regra em um sistema “saudável”.

Em tal caso, um spinlock tem um desempenho muito superior a um semáforo, porque se não houver congestionamento da fechadura, a sobrecarga de aquisição do spinlock é de apenas uma dúzia de ciclos em comparação com centenas/milhares de ciclos para um interruptor de contexto ou 10-20 milhões de ciclos para perder o resto de uma fatia de tempo.

Por outro lado, dado o elevado congestionamento, ou se o bloqueio estiver a ser mantido por longos períodos (às vezes não se pode evitar!), um spinlock irá queimar quantidades insanas de ciclos de CPU para não conseguir nada.
Um semáforo (ou mutex) é uma escolha muito melhor neste caso, pois permite que uma thread diferente execute tarefas úteis durante esse tempo. Ou, se nenhuma outra thread tem algo útil a fazer, permite que o sistema operacional estrangule a CPU e reduza o calor/conservar energia.