Articles

Spinlock vs Semáforo

Un spinlock es un bloqueo, y por tanto un mecanismo de exclusión mutua (estrictamente 1 a 1). Funciona consultando y/o modificando repetidamente una posición de memoria, normalmente de forma atómica. Esto significa que la adquisición de un spinlock es una operación «ocupada» que posiblemente quema ciclos de la CPU durante mucho tiempo (¡tal vez para siempre!) mientras que efectivamente no logra «nada».
El principal incentivo para este enfoque es el hecho de que un cambio de contexto tiene una sobrecarga equivalente a girar unos pocos cientos (o tal vez miles) de veces, por lo que si un bloqueo puede ser adquirido por la quema de unos pocos ciclos girando, esto puede en general muy bien ser más eficiente. Además, para las aplicaciones en tiempo real puede no ser aceptable bloquearse y esperar a que el planificador vuelva a ellas en algún momento lejano en el 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();

Un semáforo, por el contrario, o bien no gira en absoluto, o sólo gira durante un tiempo muy corto (como una optimización para evitar la sobrecarga de syscall). Si un semáforo no puede ser adquirido, se bloquea, cediendo el tiempo de la CPU a otro hilo que esté listo para ejecutarse. Esto puede significar, por supuesto, que pasen algunos milisegundos antes de que su hilo se programe de nuevo, pero si esto no es un problema (normalmente no lo es) entonces puede ser un enfoque muy eficiente y conservador de la CPU.

Un sistema bien diseñado normalmente tiene poca o ninguna congestión (esto significa que no todos los hilos tratan de adquirir el bloqueo exactamente al mismo tiempo). Por ejemplo, normalmente no se escribiría un código que adquiera un bloqueo, luego cargue medio megabyte de datos comprimidos de la red, decodifique y analice los datos, y finalmente modifique una referencia compartida (añadir datos a un contenedor, etc.) antes de liberar el bloqueo. En cambio, uno adquiriría el bloqueo sólo con el propósito de acceder al recurso compartido.
Como esto significa que hay considerablemente más trabajo fuera de la sección crítica que dentro de ella, naturalmente la probabilidad de que un hilo esté dentro de la sección crítica es relativamente baja, y por lo tanto pocos hilos se disputan el bloqueo al mismo tiempo. Por supuesto, de vez en cuando dos hilos intentarán adquirir el bloqueo al mismo tiempo (¡si esto no pudiera ocurrir no se necesitaría un bloqueo!), pero esto es más bien la excepción que la regla en un sistema «sano».

En tal caso, un bloqueo de giro supera en gran medida a un semáforo porque si no hay congestión de bloqueos, la sobrecarga de adquirir el bloqueo de giro es una mera docena de ciclos en comparación con cientos/miles de ciclos para un cambio de contexto o 10-20 millones de ciclos para perder el resto de una porción de tiempo.

Por otro lado, si la congestión es alta, o si el bloqueo se mantiene durante largos periodos de tiempo (¡a veces no se puede evitar!), un spinlock quemará cantidades insanas de ciclos de CPU para no conseguir nada.
Un semáforo (o mutex) es una opción mucho mejor en este caso, ya que permite que un hilo diferente ejecute tareas útiles durante ese tiempo. O, si ningún otro hilo tiene algo útil que hacer, permite al sistema operativo reducir la velocidad de la CPU y reducir el calor / conservar la energía.