Haskell: ¿Cómo funciona ''atomicModifyIORef''?
concurrency locking (2)
¿Alguien puede explicar cómo funciona atomicModifyIORef
? En particular:
(1) ¿Espera un bloqueo o lo intenta con optimismo y TVar
intentarlo si hay contención (como TVar
)?
(2) ¿Por qué la firma de atomicModifyIORef
diferente de la firma de atomicModifyIORef ? En particular, ¿cuál es esta variable extra b
?
Edición: creo que he descubierto la respuesta a (2), en que b
es un valor que se debe extraer (esto puede estar vacío si no es necesario). En un programa de un solo subproceso, saber que el valor es trivial, pero en un programa de multiproceso, uno puede querer saber cuál era el valor anterior en el momento de la función que se está aplicando. Supongo que esta es la razón por la que modifyIORef
no tiene este valor de retorno adicional (ya que los usos de modifyIORef
con este valor de retorno probablemente deberían usar atomicModifyIORef
todos modos. Todavía estoy interesado en la respuesta a (1).
¿Espera un bloqueo o lo intenta con optimismo y vuelve a intentarlo si hay contención (como TVar)?
atomicModifyIORef usa una instrucción de bloqueo en la arquitectura de hardware subyacente en la que estás, para intercambiar el puntero a un objeto Haskell asignado de una manera atómica.
En x86 usa la introducción de cas, expuesta como primitiva al lenguaje a través de atomicModifyMutVar#
, que se implementa como un servicio de tiempo de ejecución en Cmm como:
stg_atomicModifyMutVarzh
{
...
retry:
x = StgMutVar_var(mv);
StgThunk_payload(z,1) = x;
#ifdef THREADED_RTS
(h) = foreign "C" cas(mv + SIZEOF_StgHeader + OFFSET_StgMutVar_var, x, y) [];
if (h != x) { goto retry; }
#else
StgMutVar_var(mv) = y;
#endif
...
}
Es decir, intentará hacer el intercambio, y reintentar de lo contrario.
La implementación de cas como primitivo muestra cómo llegamos al metal:
/*
* Compare-and-swap. Atomically does this:
*/
EXTERN_INLINE StgWord cas(StgVolatilePtr p, StgWord o, StgWord n);
/*
* CMPXCHG - the single-word atomic compare-and-exchange instruction. Used
* in the STM implementation.
*/
EXTERN_INLINE StgWord
cas(StgVolatilePtr p, StgWord o, StgWord n)
{
#if i386_HOST_ARCH || x86_64_HOST_ARCH
__asm__ __volatile__ (
"lock/ncmpxchg %3,%1"
:"=a"(o), "=m" (*(volatile unsigned int *)p)
:"0" (o), "r" (n));
return o;
#elif arm_HOST_ARCH && defined(arm_HOST_ARCH_PRE_ARMv6)
StgWord r;
arm_atomic_spin_lock();
r = *p;
if (r == o) { *p = n; }
arm_atomic_spin_unlock();
return r;
#elif !defined(WITHSMP)
StgWord result;
result = *p;
if (result == o) {
*p = n;
}
return result;
Entonces, pueden ver que es capaz de usar una instrucción atómica en Intel, en otras arquitecturas se usarán diferentes mecanismos. El tiempo de ejecución lo volverá a intentar.
atomicModifyIORef
toma un r :: IORef a
y una función f :: a -> (a, b)
y hace lo siguiente:
Lee el valor de r
y aplica f
a este valor, produciendo (a'',b)
. Luego, r
se actualiza con el nuevo valor a''
mientras que b
es el valor de retorno. Este acceso de lectura y escritura se realiza de forma atómica.
Por supuesto, esta atomicidad solo funciona si todos los accesos a r
se realizan a través de atomicModifyIORef
. Tenga en cuenta que puede encontrar esta información mirando la fuente [1].