multithreading - lock - ¿Cuál es el bloqueo mutex básico o el entero atómico más eficiente?
pthread mutex (7)
Para algo simple como un contador si múltiples hilos aumentarán el número. Leí que los bloqueos mutex pueden disminuir la eficiencia ya que los hilos tienen que esperar. Entonces, para mí, un contador atómico sería el más eficiente, pero ¿leí que internamente es básicamente un bloqueo? Así que supongo que estoy confundido de que una de las dos podría ser más eficiente que la otra.
El entero atómico es un objeto de modo de usuario, ya que es mucho más eficiente que un mutex que se ejecuta en modo kernel . El alcance del entero atómico es una aplicación única, mientras que el alcance del mutex es para todo el software en ejecución en la máquina.
La mayoría de los procesadores han admitido una lectura o escritura atómica, y con frecuencia un cmp & swap atómico. Esto significa que el propio procesador escribe o lee el último valor en una sola operación, y es posible que se pierdan algunos ciclos en comparación con un acceso de entero normal, especialmente porque el compilador no puede optimizar las operaciones atómicas casi tan bien como de costumbre.
Por otro lado, un mutex es una serie de líneas de código para entrar y salir, y durante esa ejecución, otros procesadores que acceden a la misma ubicación están totalmente bloqueados, por lo que es evidente que hay una gran sobrecarga en ellos. En el código de alto nivel no optimizado, el ingreso / salida mutex y el atómico serán llamadas de función, pero para el mutex, se bloqueará cualquier procesador de la competencia mientras su función de ingreso mutex regrese, y mientras se inicie la función de salida. Para atómico, solo se bloquea la duración de la operación real. La optimización debería reducir ese costo, pero no todo.
Si está intentando incrementar, entonces su procesador moderno probablemente admita el incremento / decremento atómico, lo que será excelente.
Si no lo hace, entonces se implementa utilizando el procesador atómico cmp & swap, o utilizando un mutex.
Mutex:
get the lock
read
increment
write
release the lock
Cmp atómico y swap:
atomic read the value
calc the increment
do{
atomic cmpswap value, increment
recalc the increment
}while the cmp&swap did not see the expected value
Entonces, esta segunda versión tiene un bucle [en caso de que otro procesador incremente el valor entre nuestras operaciones atómicas, así que el valor ya no coincide, y el incremento sería incorrecto] que puede alargarse [si hay muchos competidores], pero en general aún debería ser más rápido que la versión de exclusión mutua, pero la versión de exclusión mutua puede permitir que el procesador cambie de tarea.
Las clases de variables atómicas en Java pueden aprovechar las instrucciones de comparación e intercambio proporcionadas por el procesador.
Aquí hay una descripción detallada de las diferencias: http://www.ibm.com/developerworks/library/j-jtp11234/
Las operaciones atómicas aprovechan el soporte del procesador (compara e intercambia instrucciones) y no usan bloqueos en absoluto, mientras que los bloqueos dependen más del sistema operativo y tienen un rendimiento diferente, por ejemplo, Win y Linux.
Los bloqueos realmente suspenden la ejecución del subproceso, liberando recursos de la CPU para otras tareas, pero incurren en una sobrecarga obvia de cambio de contexto al detener / reiniciar el subproceso. Por el contrario, los subprocesos que intentan operaciones atómicas no esperan y continúan intentando hasta el éxito (lo que se denomina ocupado en espera), por lo que no incurren en sobrecarga de cambio de contexto, pero tampoco liberan recursos de la CPU.
En resumen, en general, las operaciones atómicas son más rápidas si la contención entre hilos es suficientemente baja. Definitivamente, debería realizar evaluaciones comparativas, ya que no existe otro método confiable para saber cuál es la sobrecarga más baja entre el cambio de contexto y la espera.
Si tiene un contador para el que se admiten operaciones atómicas, será más eficiente que un mutex.
Técnicamente, el atómico bloqueará el bus de memoria en la mayoría de las plataformas. Sin embargo, hay dos detalles que mejoran:
- Es imposible suspender un hilo durante el bloqueo del bus de memoria, pero es posible suspender un hilo durante un bloqueo mutex. Esto es lo que le permite obtener una garantía sin bloqueo (que no dice nada sobre no bloquear, solo garantiza que al menos un hilo progrese).
- Los Mutexes eventualmente terminan siendo implementados con atomics. Dado que necesita al menos una operación atómica para bloquear una exclusión mutua, y una operación atómica para desbloquear una exclusión mutua, se necesita al menos dos veces el tiempo para realizar un bloqueo mutex, incluso en el mejor de los casos.
Una implementación de exclusión mínima (que cumpla con los estándares) requiere 2 ingredientes básicos:
- Una forma de transmitir atómicamente un cambio de estado entre hilos (el estado ''bloqueado'')
- Barreras de memoria para hacer cumplir las operaciones de memoria protegidas por el mutex para permanecer dentro del área protegida.
No hay forma de hacer que esto sea más sencillo debido a la relación ''sincroniza con'' que requiere el estándar de C ++.
Una implementación mínima (correcta) podría verse así:
class mutex {
std::atomic<bool> flag{false};
public:
void lock()
{
while (flag.exchange(true, std::memory_order_relaxed));
std::atomic_thread_fence(std::memory_order_acquire);
}
void unlock()
{
std::atomic_thread_fence(std::memory_order_release);
flag.store(false, std::memory_order_relaxed);
}
};
Debido a su simplicidad (no puede suspender el hilo de ejecución), es probable que, bajo la contención, esta implementación supere a std::mutex
. Pero incluso entonces, es fácil ver que cada incremento de entero, protegido por este mutex, requiere las siguientes operaciones:
- Una tienda
atomic
para liberar el mutex. - una comparación e intercambio
atomic
(lectura-modificación-escritura) para adquirir el mutex (posiblemente varias veces) - un incremento entero
Si lo compara con un estándar std::atomic<int>
que se incrementa con una sola lectura-modificación (incondicional) de lectura (por ejemplo, fetch_add
), es razonable esperar que se fetch_add
una operación atómica (utilizando el mismo modelo de ordenamiento) superará el caso en el que se utiliza un mutex.
Mutex
es una semántica de nivel de kernel que proporciona exclusión mutua incluso en el Process level
. Tenga en cuenta que puede ser útil para extender la exclusión mutua a través de los límites del proceso y no solo dentro de un proceso (para subprocesos). Es más costoso.
El contador atómico, por ejemplo, AtomicInteger
, se basa en CAS y, por lo general, intenta hacer una operación hasta que tenga éxito. Básicamente, en este caso, los hilos compiten o compiten para aumentar / disminuir el valor de forma atómica. Aquí, puede ver buenos ciclos de CPU utilizados por un subproceso que intenta operar en un valor actual.
Ya que desea mantener el contador, AtomicInteger / AtomicLong será el mejor para su caso de uso.