Estoy tratando de responder a esto yo mismo, después de revisar varios recursos en línea (por ejemplo, este y este ), el Estándar C ++ 11, así como las respuestas que se dan aquí.
Las preguntas relacionadas se fusionan (por ejemplo, " ¿por qué! ¿Se esperaba? " Se fusiona con "¿por qué poner compare_exchange_weak () en un bucle? ") Y las respuestas se dan en consecuencia.
¿Por qué compare_exchange_weak () tiene que estar en un bucle en casi todos los usos?
Patrón típico A
Necesita lograr una actualización atómica basada en el valor de la variable atómica. Un error indica que la variable no se actualiza con nuestro valor deseado y queremos volver a intentarlo. Tenga en cuenta que realmente no nos importa si falla debido a una escritura concurrente o una falla falsa. Pero nos importa que seamos nosotros los que hagamos este cambio.
expected = current.load();
do desired = function(expected);
while (!current.compare_exchange_weak(expected, desired));
Un ejemplo del mundo real es que varios subprocesos agreguen un elemento a una lista enlazada individualmente al mismo tiempo. Cada hilo primero carga el puntero de la cabeza, asigna un nuevo nodo y agrega la cabeza a este nuevo nodo. Finalmente, intenta intercambiar el nuevo nodo con la cabeza.
Otro ejemplo es implementar mutex usando std::atomic<bool>
. A lo sumo un hilo puede entrar en la sección crítica a la vez, dependiendo de la rosca primero establecer current
a true
y salir del bucle.
Patrón típico B
Este es en realidad el patrón mencionado en el libro de Anthony. A diferencia del patrón A, desea que la variable atómica se actualice una vez, pero no le importa quién lo haga. Siempre que no esté actualizado, vuelve a intentarlo. Esto se usa normalmente con variables booleanas. Por ejemplo, necesita implementar un disparador para que una máquina de estado siga adelante. Qué hilo aprieta el gatillo es independiente.
expected = false;
while (!current.compare_exchange_weak(expected, true) && !expected);
Tenga en cuenta que generalmente no podemos usar este patrón para implementar un mutex. De lo contrario, varios subprocesos pueden estar dentro de la sección crítica al mismo tiempo.
Dicho esto, debería ser raro usarlo compare_exchange_weak()
fuera de un bucle. Por el contrario, hay casos en los que se está utilizando la versión fuerte. P.ej,
bool criticalSection_tryEnter(lock)
{
bool flag = false;
return lock.compare_exchange_strong(flag, true);
}
compare_exchange_weak
no es apropiado aquí porque cuando regresa debido a una falla falsa, es probable que nadie ocupe la sección crítica todavía.
¿Hilo hambriento?
Un punto que vale la pena mencionar es que ¿qué sucede si continúan ocurriendo fallas espúreas y así el hilo se muere de hambre? En teoría, podría ocurrir en plataformas cuando compare_exchange_XXX()
se implementa como una secuencia de instrucciones (por ejemplo, LL / SC). El acceso frecuente a la misma línea de caché entre LL y SC producirá fallas falsas continuas. Un ejemplo más realista se debe a una programación tonta en la que todos los subprocesos simultáneos se intercalan de la siguiente manera.
Time
| thread 1 (LL)
| thread 2 (LL)
| thread 1 (compare, SC), fails spuriously due to thread 2's LL
| thread 1 (LL)
| thread 2 (compare, SC), fails spuriously due to thread 1's LL
| thread 2 (LL)
v ..
Puede suceder
No sucederá para siempre, afortunadamente, gracias a lo que requiere C ++ 11:
Las implementaciones deben asegurar que las operaciones débiles de comparación e intercambio no devuelvan constantemente falso a menos que el objeto atómico tenga un valor diferente al esperado o haya modificaciones concurrentes en el objeto atómico.
¿Por qué nos molestamos en usar compare_exchange_weak () y escribir el bucle nosotros mismos? Podemos simplemente usar compare_exchange_strong ().
Depende.
Caso 1: Cuando ambos deben usarse dentro de un bucle. C ++ 11 dice:
Cuando se realiza una comparación e intercambio, la versión débil producirá un mejor rendimiento en algunas plataformas.
En x86 (al menos actualmente. Quizás algún día recurra a un esquema similar al LL / SC para mejorar el rendimiento cuando se introduzcan más núcleos), la versión débil y la fuerte son esencialmente iguales porque ambas se reducen a una sola instrucción cmpxchg
. En algunas otras plataformas donde compare_exchange_XXX()
no se implementa de forma atómica (aquí lo que significa que no existe una sola primitiva de hardware), la versión débil dentro del ciclo puede ganar la batalla porque la fuerte tendrá que manejar las fallas espúreas y reintentar en consecuencia.
Pero,
En raras ocasiones, es posible que prefieren compare_exchange_strong()
más compare_exchange_weak()
aún en un bucle. Por ejemplo, cuando hay muchas cosas que hacer entre la variable atómica se carga y se intercambia un nuevo valor calculado (ver function()
arriba). Si la variable atómica en sí misma no cambia con frecuencia, no necesitamos repetir el costoso cálculo para cada falla espuria. En cambio, podemos esperar compare_exchange_strong()
"absorber" tales fallas y solo repetimos el cálculo cuando falla debido a un cambio de valor real.
Caso 2: Cuando solo compare_exchange_weak()
necesita usarse dentro de un bucle. C ++ 11 también dice:
Cuando una comparación e intercambio débil requeriría un bucle y uno fuerte no, es preferible el fuerte.
Este suele ser el caso cuando se realiza un ciclo solo para eliminar fallas falsas de la versión débil. Vuelva a intentarlo hasta que el intercambio sea exitoso o fallido debido a la escritura simultánea.
expected = false;
while (!current.compare_exchange_weak(expected, true) && !expected);
En el mejor de los casos, se trata de reinventar las ruedas y funcionar igual que compare_exchange_strong()
. ¿Peor? Este enfoque no logra aprovechar al máximo las máquinas que brindan comparación e intercambio no espurios en hardware .
Por último, si hace un bucle para otras cosas (por ejemplo, vea el "Patrón típico A" más arriba), entonces hay una buena posibilidad de que compare_exchange_strong()
también se coloque en un bucle, lo que nos lleva de vuelta al caso anterior.