siko
Implementé la clase SpinLock, como sigue
struct Node {
int number;
std::atomic_bool latch;
void add() {
lock();
number++;
unlock();
}
void lock() {
bool unlatched = false;
while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire));
}
void unlock() {
latch.store(false , std::memory_order_release);
}
};
Implementé la clase anterior e hice dos subprocesos que llaman al método add() de una misma instancia de la clase Node 10 millones de veces por subproceso.
el resultado es, lamentablemente, no 20 millones. ¿Que me estoy perdiendo aqui?
El problema es ese compare_exchange_weak
actualiza el unlatched
variable una vez que falla. De la documentación de compare_exchange_weak
:
Compara el contenido del valor contenido del objeto atómico con el esperado: – si es verdadero, reemplaza el valor contenido con val (como store).
– si es falso, reemplaza esperado con el valor contenido.
Es decir, después de la primera falla compare_exchange_weak
, unlatched
se actualizará a true
por lo que la siguiente iteración del bucle intentará compare_exchange_weak
true
con true
. Esto tuvo éxito y acaba de tomar un candado que estaba en manos de otro subproceso.
Solución: asegúrese de configurar unlatched
de regreso false
antes de cada compare_exchange_weak
p.ej:
while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire)) {
unlatched = false;
}
-
+1, y nota para el OP, el constructor predeterminado de
Node
voluntad no inicializar valornumber
tal como se presenta. Es decirNode node;
no dejará un valor determinista ennode.number
para iniciar su secuencia. Necesitas un constructor de tal uso,Node() : number() {}
es suficiente. verlo en vivo– WhozCraig
27 de octubre de 2014 a las 8:34
-
¡¡Gracias por tus comentarios!! ¡Y encontré mis errores en la piscina! gracias
– syko
27 de octubre de 2014 a las 8:42
-
tenga en cuenta que el bucle también requiere
__mm_pause()
para habilitar los hiperprocesos.– Antón
27 de octubre de 2014 a las 8:57
mikemb
Como menciona @gexicide, el problema es que el compare_exchange
las funciones actualizan el expected
variable con el valor actual de la variable atómica. Esa es también la razón por la que tienes que usar la variable local unlatched
en primer lugar. Para solucionar esto puedes configurar unlatched
vuelve a falso en cada iteración del bucle.
Sin embargo, en lugar de usar compare_exchange
para algo para lo que su interfaz no es adecuada, es mucho más simple de usar std::atomic_flag
en cambio:
class SpinLock {
std::atomic_flag locked = ATOMIC_FLAG_INIT ;
public:
void lock() {
while (locked.test_and_set(std::memory_order_acquire)) { ; }
}
void unlock() {
locked.clear(std::memory_order_release);
}
};
Fuente: preferencia cp
Especificar manualmente el orden de la memoria es solo un ajuste menor de rendimiento potencial, que copié de la fuente. Si la simplicidad es más importante que el último bit de rendimiento, puede ceñirse a los valores predeterminados y simplemente llamar locked.test_and_set() / locked.clear()
.
Por cierto.: std::atomic_flag
es el único tipo que está garantizado para estar libre de bloqueo, aunque no conozco ninguna plataforma, donde las operaciones en std::atomic_bool
no están libres de bloqueo.
Actualizar: Como se explica en los comentarios de @David Schwartz, @Anton y @Technik Empire, el bucle vacío tiene algunos efectos no deseados, como predicción errónea de ramas, inanición de subprocesos en los procesadores HT y un consumo de energía demasiado alto; en resumen, es una forma bastante ineficiente de Espere. El impacto y la solución son específicos de la arquitectura, la plataforma y la aplicación. No soy un experto, pero la solución habitual parece ser agregar un cpu_relax()
en linux o YieldProcessor()
en las ventanas al cuerpo del bucle.
EDIT2: Para que quede claro: la versión portátil presentada aquí (sin las instrucciones especiales de cpu_relax, etc.) ya debería ser lo suficientemente buena para muchas aplicaciones. Si tu SpinLock
gira mucho porque otra persona mantiene el bloqueo durante mucho tiempo (lo que ya podría indicar un problema general de diseño), probablemente sea mejor usar un mutex normal de todos modos.
-
simplemente agregue una llamada std::this_thread::yield en el ciclo while: es.cppreference.com/w/cpp/thread/yield
– Martín Gerhardy
6 de junio de 2016 a las 7:58
-
@Martin: Pensé en eso, pero
std::this_thread::yield()
es una llamada al sistema bastante pesada, así que no estoy seguro si la pondría en el cuerpo del bucle de un spinlock. Mi suposición (no probada) es que en la mayoría de las situaciones, donde eso estaría bien, querrás usar un std::mutex (o similar) para empezar.– Mike MB
6 de junio de 2016 a las 8:23
-
@MartinGerhardy No tiene sentido usar el rendimiento en un bloqueo giratorio. El objetivo de los spin-locks es evitar cambios de contexto que son costosos en pequeñas secciones críticas.
– Jorge Bellón
5 de enero de 2017 a las 10:13
-
@rox ese no es mi punto. La definición técnica de spin mutex es un bloqueo que no libera la CPU si otros ya la han adquirido. En los sistemas de un solo núcleo, generalmente no usa spinlocks sino un mutex regular.
– Jorge Bellón
6 de diciembre de 2017 a las 17:32
-
@JorgeBellón Hasta donde yo sé, la implementación de glibc usa algo similar para producir: github.com/lattera/glibc/blob/master/mach/spin-solid.c . No estoy seguro de dónde encontraste la definición.
– Rox
7 de diciembre de 2017 a las 5:01
Tenga cuidado, este es el peor spinlock que posiblemente pueda implementar. Entre otros problemas: 1) Cuando finalmente adquieres el
lock
te llevas a la madre de todas las ramas mal vaticinadas dejando elwhile
bucle, que es el peor momento posible para tal cosa. 2) Ellock
La función puede privar a otro subproceso que se ejecuta en el mismo núcleo virtual en una CPU hiperproceso.–David Schwartz
20/04/2015 a las 10:50
@DavidSchwartz, Gracias por sus comentarios. ¿Puedo preguntar más sobre los problemas que mencionaste? 2). Esa función de bloqueo puede privar a otro hilo (¡así es!, y está destinado a hacer eso ya que estoy seguro de que la vida útil del bloqueo es muy corta). Puedo usar algunos contadores de giros para resolver este problema, ¿verdad? 1). ¿Por qué tomo ‘la madre de todos los brabches mal predicados’ en este código? ¿Y cómo puedo mejorarlo? ¿tienes algún comentario al respecto? Gracias de nuevo
– syko
26 de abril de 2015 a las 5:01
La pausa evita la ejecución especulativa, eliminando la penalización por error de predicción de rama. (Y, por cierto, si no conoce este tipo de cosas de adentro hacia afuera, no tiene por qué escribir spinlocks. Cometerá todos los errores y ni siquiera se dará cuenta de que tenía otras opciones).
–David Schwartz
27 de abril de 2015 a las 8:50
@DavidSchwartz, estoy interesado en escribir este tipo de cosas, pero no las conozco de adentro hacia afuera. ¿Puede recomendar cómo una persona puede hacerse saber esto de adentro hacia afuera?
– tiro en código
1 de junio de 2016 a las 0:22
@DavidSchwartz, parece que la única forma de adquirir el conocimiento suficiente para poder intentar escribir este tipo de código es intentar escribir este tipo de código.
– tiro en código
04/06/2016 a las 11:41