Quería saber lo mismo, así que lo medí. En mi caja (procesador AMD FX (tm) -8150 de ocho núcleos a 3.612361 GHz), bloquear y desbloquear un mutex desbloqueado que está en su propia línea de caché y ya está en caché, toma 47 relojes (13 ns).
Debido a la sincronización entre dos núcleos (utilicé CPU # 0 y # 1), solo pude llamar a un par de bloqueo / desbloqueo una vez cada 102 ns en dos subprocesos, por lo que una vez cada 51 ns, de lo que se puede concluir que toma aproximadamente 38 ns para recuperarse después de que un hilo se desbloquea antes de que el siguiente hilo pueda bloquearlo nuevamente.
El programa que utilicé para investigar esto se puede encontrar aquí:
https://github.com/CarloWood/ai-statefultask-testsuite/blob/b69b112e2e91d35b56a39f41809d3e3de2f9e4b8/src/mutex_test.cxx
Tenga en cuenta que tiene algunos valores codificados específicamente para mi cuadro (xrange, yrange y rdtsc de arriba), por lo que probablemente tenga que experimentar con él antes de que funcione para usted.
El gráfico que produce en ese estado es:
Esto muestra el resultado de las ejecuciones de referencia en el siguiente código:
uint64_t do_Ndec(int thread, int loop_count)
{
uint64_t start;
uint64_t end;
int __d0;
asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (start) : : "%rdx");
mutex.lock();
mutex.unlock();
asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (end) : : "%rdx");
asm volatile ("\n1:\n\tdecl %%ecx\n\tjnz 1b" : "=c" (__d0) : "c" (loop_count - thread) : "cc");
return end - start;
}
Las dos llamadas rdtsc miden la cantidad de relojes necesarios para bloquear y desbloquear 'mutex' (con una sobrecarga de 39 relojes para las llamadas rdtsc en mi casilla). El tercer asm es un bucle de retraso. El tamaño del bucle de retardo es 1 recuento menor para el subproceso 1 que para el subproceso 0, por lo que el subproceso 1 es ligeramente más rápido.
La función anterior se llama en un ciclo cerrado de tamaño 100,000. A pesar de que la función es ligeramente más rápida para el subproceso 1, ambos bucles se sincronizan debido a la llamada al mutex. Esto es visible en el gráfico por el hecho de que el número de relojes medidos para el par de bloqueo / desbloqueo es ligeramente mayor para el hilo 1, para tener en cuenta el retraso más corto en el bucle debajo de él.
En el gráfico anterior, el punto inferior derecho es una medición con un retraso loop_count de 150, y luego, siguiendo los puntos en la parte inferior, hacia la izquierda, el loop_count se reduce en uno en cada medición. Cuando se convierte en 77, la función se llama cada 102 ns en ambos hilos. Si posteriormente loop_count se reduce aún más, ya no es posible sincronizar los subprocesos y el mutex comienza a bloquearse la mayor parte del tiempo, lo que resulta en una mayor cantidad de relojes que se necesitan para bloquear / desbloquear. Además, el tiempo promedio de la llamada a la función aumenta debido a esto; entonces los puntos de la trama ahora suben y vuelven a la derecha nuevamente.
De esto podemos concluir que bloquear y desbloquear un mutex cada 50 ns no es un problema en mi caja.
En general, mi conclusión es que la respuesta a la pregunta de OP es que agregar más mutexes es mejor siempre que eso resulte en menos contención.
Intente bloquear mutexes lo más corto posible. La única razón para colocarlos, digamos, fuera de un bucle sería si ese bucle se repite más rápido que una vez cada 100 ns (o más bien, el número de subprocesos que desean ejecutar ese bucle al mismo tiempo multiplicado por 50 ns) o cuando 13 ns veces el tamaño del bucle es más demorado que el retraso que se obtiene por contención
EDITAR: ahora tengo mucho más conocimiento sobre el tema y empiezo a dudar de la conclusión que presenté aquí. En primer lugar, las CPU 0 y 1 son hiperprocesadas; A pesar de que AMD afirma tener 8 núcleos reales, ciertamente hay algo muy sospechoso porque los retrasos entre otros dos núcleos son mucho mayores (es decir, 0 y 1 forman un par, al igual que 2 y 3, 4 y 5, y 6 y 7 ) En segundo lugar, el std :: mutex se implementa de manera que hace girar los bloqueos por un momento antes de realizar llamadas al sistema cuando no puede obtener el bloqueo de inmediato en un mutex (que sin duda será extremadamente lento). Entonces, lo que he medido aquí es la situación más ideal y, en la práctica, el bloqueo y desbloqueo puede tomar drásticamente más tiempo por bloqueo / desbloqueo.
En pocas palabras, un mutex se implementa con atómicos. Para sincronizar atómicas entre núcleos, debe bloquearse un bus interno que congela la línea de caché correspondiente durante varios cientos de ciclos de reloj. En el caso de que no se pueda obtener un bloqueo, se debe realizar una llamada al sistema para poner el hilo en suspensión; eso es obviamente extremadamente lento (las llamadas al sistema son del orden de 10 mircosecondos). Normalmente eso no es realmente un problema porque ese hilo tiene que dormir de todos modos, pero podría ser un problema con una alta contención donde un hilo no puede obtener el bloqueo por el tiempo que normalmente gira y también lo hace el sistema, pero PUEDE toma la cerradura poco después. Por ejemplo, si varios subprocesos bloquean y desbloquean un mutex en un bucle cerrado y cada uno mantiene el bloqueo durante 1 microsegundo más o menos, entonces podrían ser ralentizados enormemente por el hecho de que son constantemente dormidos y despertados nuevamente. Además, una vez que un subproceso duerme y otro subproceso tiene que despertarlo, ese subproceso debe realizar una llamada al sistema y se retrasa ~ 10 microsegundos; esta demora ocurre mientras se desbloquea un mutex cuando otro hilo está esperando ese mutex en el kernel (después de que el giro tomó demasiado tiempo).