laslowh
¿Dónde puedo encontrar documentación para pthread mutexes “adaptables”? El símbolo PTHREAD_MUTEX_ADAPTIVE_NP está definido en mi sistema, pero el solo documentación Puedo encontrar en línea que no dice nada sobre qué es un mutex adaptativo, o cuándo es apropiado usarlo.
Entonces… ¿qué es y cuándo debo usarlo?
Como referencia, mi versión de libc es:
GNU C Library (Ubuntu EGLIBC 2.15-0ubuntu10.5) stable release version 2.15, by Roland McGrath et al.
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 4.6.3.
Compiled on a Linux 3.2.50 system on 2013-09-30.
Available extensions:
crypt add-on version 2.1 by Michael Glad and others
GNU Libidn by Simon Josefsson
Native POSIX Threads Library by Ulrich Drepper et al
BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<http://www.debian.org/Bugs/>.
y “uname -a” da
Linux desktop 3.2.0-55-generic #85-Ubuntu SMP Wed Oct 2 12:29:27 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux
Kaz
PTHREAD_MUTEX_ADAPTIVE_NP
es algo que inventé mientras trabajaba en el rol de colaborador de glibc para hacer que LinuxThreads sea más confiable y funcione mejor. LinuxThreads fue el predecesor de glibc biblioteca NPTLdesarrollado originalmente como una biblioteca independiente por Xavier Leroy, quien también es conocido como uno de los creadores de OCaml.
El mutex adaptativo sobrevivió en NTPL esencialmente sin modificar: el código es casi idéntico, incluidas las constantes mágicas para el suavizado del estimador y el giro máximo relativo al estimador.
Bajo SMP, cuando va a adquirir un mutex y ve que está bloqueado, puede ser subóptimo simplemente darse por vencido y llamar al kernel para bloquear. Si el propietario de la cerradura solo tiene la cerradura para unas pocas instrucciones, es más económico esperar la ejecución de esas instrucciones y luego adquirir la cerradura con una operación atómica, en lugar de gastar cientos de ciclos adicionales haciendo una llamada al sistema. .
Los desarrolladores del kernel lo saben muy bien, y esa es una de las razones por las que tenemos spinlocks en el kernel de Linux para secciones críticas rápidas. (Entre las otras razones está, por supuesto, que el código que no puede dormir, porque está en un contexto de interrupción, puede adquirir spinlocks).
La pregunta es, ¿cuánto tiempo debes esperar? Si gira para siempre hasta que se adquiere el bloqueo, eso puede ser subóptimo. Los programas de espacio de usuario no están bien escritos como el código del kernel (tos). Podrían tener largas secciones críticas. Tampoco pueden deshabilitar la preferencia; a veces, las secciones críticas explotan debido a un cambio de contexto. (Los subprocesos POSIX ahora brindan herramientas en tiempo real para lidiar con esto: puede colocar los subprocesos en una prioridad en tiempo real y programación FIFO y demás, además de configurar la afinidad del procesador).
Creo que experimentamos con recuentos de iteraciones fijos, pero luego tuve esta idea: ¿por qué deberíamos adivinar, cuando podemos medir? ¿Por qué no implementamos un estimador suavizado de la duración del bloqueo, similar a lo que hacemos para el estimador de tiempo de espera de retransmisión (RTO) de TCP? Cada vez que giramos un candado, debemos medir cuántos giros realmente se necesitaron para adquirirlo. Además, no deberíamos girar para siempre: tal vez deberíamos girar solo como máximo el doble del valor actual del estimador. Cuando tomamos una medida, podemos suavizarla exponencialmente, en solo unas pocas instrucciones: toma una fracción del valor anterior y del nuevo valor, y súmalos, que es lo mismo que sumar una fracción de su diferencia para volver al estimador: digamos, estimator += (new_val - estimator)/8
para una mezcla de 1/8 a 7/8 entre el valor antiguo y el nuevo.
Puedes pensar en esto como un perro guardián. Suponga que el estimador le dice que la cerradura, en promedio, tarda 80 giros en adquirirse. Entonces, puede estar bastante seguro de que si ha ejecutado 160 giros, entonces algo está mal: el propietario del bloqueo está ejecutando un caso excepcionalmente largo, o tal vez haya fallado en la página o se haya adelantado de otra manera. En este punto, el subproceso en espera reduce sus pérdidas y llama al kernel para bloquear.
Sin medición, no puede hacer esto con precisión: no hay un valor “único para todos”. Digamos que un límite fijo de 200 giros sería subóptimo en un programa cuyas secciones críticas son tan cortas que casi siempre se puede obtener un bloqueo después de esperar solo 10 giros. La función de bloqueo mutex quemaría 200 iteraciones cada vez que haya un tiempo de espera anómalo, en lugar de rendirse agradablemente en, digamos, 20 y ciclos de ahorro.
Este enfoque adaptativo es especializado, en el sentido de que no funcionará para todos los bloqueos en todos los programas, por lo que está empaquetado como un tipo de mutex especial. Por ejemplo, no funcionará muy bien para los programas que bloquean los mutex durante períodos prolongados: períodos tan prolongados que se desperdicia más tiempo de CPU girando en los valores grandes del estimador de lo que se hubiera invertido en el kernel. El enfoque tampoco es adecuado para monoprocesadores: todos los subprocesos, además del que intenta obtener el bloqueo, están suspendidos en el núcleo. El enfoque tampoco es adecuado en situaciones en las que la equidad es importante: es un candado oportunista. No importa cuántos otros subprocesos hayan estado esperando, sin importar cuánto tiempo o cuál sea su prioridad, puede aparecer un nuevo subproceso y arrebatar el bloqueo.
Si tiene un código que se comporta muy bien con secciones críticas cortas que son muy controvertidas y está buscando un mejor rendimiento en SMP, puede valer la pena probar el mutex adaptativo.
-
En el código fuente, veo que está ocupado en bucles usando nops. ¿Hay alguna razón por la que no está ocupado con
sched_yield
me gusta WebKit ¿lo hace?– Hongli
11 de mayo de 2016 a las 1:21
-
@HongLi usando
sched_yield
en un bucle está obsoleto. Linux desarrolló futexes hace aproximadamente una década y media. Con un futex, podemos usar una operación atómica (sin intervención del kernel) para obtener un bloqueo. Si eso falla, podemos ir al núcleo para esperar en el futex (en lugar de ejecutar un desperdiciosched_yield
llamadas que no esperan nada). También podemos girar en un futex: es decir, probar la operación atómica varias veces, antes de llamar a la operación de espera de futex. El enfoque de WebKit está obsoleto, basado en conceptos obsoletos.– Kaz
11 mayo 2016 a las 16:28
-
@Kaz Gracias por la respuesta. Entiendo que
sched_yield
no espera nada, pero la publicación del blog de WebKit demuestra que están ocupados en buclesched_yield
con el fin de optimizar para casos microcontenidos: casos contenciosos en los que la sección crítica es corta. si unsched_yield
tiene una sobrecarga de tiempo menos constante que una llamada al sistema futex, entonces me parece plausible que esté ocupado en buclesched_yield
para un número corto de iteraciones es beneficioso cuando se optimiza para casos microcontenidos. ¿Estás de acuerdo con esto? Si no es así, ¿está diciendo que la sobrecarga constante de una llamada al sistema futex es muy baja?– Hongli
18 de mayo de 2016 a las 1:45
-
@Hongli Con
futex
tienes que hacer syscall en ambos hilos de bloqueo y hilo de desbloqueo. Consched_yield
solo necesita una llamada al sistema en el hilo de bloqueo. El problema es esesched_yield
es impredecible y puede brindarle resultados diferentes debido a que el programador duda sobre la migración de subprocesos de un núcleo a otro que está muy lejos en el sentido del tiempo de transferencia de caché.– StaceyGirl
26 de junio de 2021 a las 17:48
Sebastián
El símbolo se menciona allí:
http://elias.rhi.hi.is/libc/Mutexes.html
“LinuxThreads solo admite un atributo de exclusión mutua: el tipo de exclusión mutua, que es PTHREAD_MUTEX_ADAPTIVE_NP para exclusiones mutuas “rápidas”, PTHREAD_MUTEX_RECURSIVE_NP para exclusiones mutuas “recursivas”, PTHREAD_MUTEX_TIMED_NP para exclusiones mutuas “temporizadas”, o PTHREAD_MUTEX_ERRORCHECK_NP para exclusiones mutuas de “comprobación de errores”. Como indica el sufijo NP , esta es una extensión no portátil del estándar POSIX y no debe emplearse en programas portátiles.
El tipo de exclusión mutua determina lo que sucede si un subproceso intenta bloquear una exclusión mutua que ya posee con pthread_mutex_lock. Si el mutex es del tipo “rápido”, pthread_mutex_lock simplemente suspende el subproceso de llamada para siempre. Si el mutex es del tipo “comprobación de errores”, pthread_mutex_lock regresa inmediatamente con el código de error EDEADLK. Si el mutex es del tipo “recursivo”, la llamada a pthread_mutex_lock regresa inmediatamente con un código de retorno exitoso. El número de veces que el subproceso que posee la exclusión mutua lo ha bloqueado se registra en la exclusión mutua. El subproceso propietario debe llamar a pthread_mutex_unlock la misma cantidad de veces antes de que el mutex vuelva al estado desbloqueado.
El tipo de exclusión mutua predeterminado es “temporizado”, es decir, PTHREAD_MUTEX_TIMED_NP”.
EDITAR: actualizado con información encontrada por jthill (¡gracias!)
Se puede encontrar un poco más de información sobre las banderas mutex y PTHREAD_MUTEX_ADAPTIVE_NP aquí:
“El PTHRED_MUTEX_ADAPTIVE_NP es un nuevo mutex que está diseñado para un alto rendimiento sacrificando la equidad e incluso los ciclos de la CPU. Este mutex no transfiere la propiedad a un subproceso en espera, sino que permite la competencia. Además, sobre un kernel SMP, la operación de bloqueo utiliza girar para volver a intentar el bloqueo para evitar el costo de la cancelación inmediata”.
Lo que básicamente sugiere lo siguiente: en caso de que se desee un alto rendimiento, dicho mutex se puede implementar y requiere consideraciones adicionales de la lógica del subproceso debido a su propia naturaleza. Tendrá que diseñar un algoritmo que pueda usar estas propiedades, lo que dará como resultado un alto rendimiento. Algo que se equilibra la carga desde dentro (en contraposición a “desde el kernel”) donde el orden de ejecución no es importante.
Había un libro muy bueno para la programación multiproceso de linux/unix cuyo nombre se me escapa. Si lo encuentro lo actualizo.
-
Sí, quise vincular a esa página en mi pregunta, corregido. Mi problema con eso es que no me dice nada sobre qué es o qué hace cada uno de esos tipos. Y más abajo en la página, da la impresión de que las únicas diferencias son lo que sucede cuando un subproceso intenta volver a bloquear un mutex. Estoy bastante seguro de que hay otras diferencias.
– laslowh
8 de noviembre de 2013 a las 16:28
-
Bueno, el siguiente párrafo en la página me parece bastante claro. El valor seleccionado cambia el comportamiento de la función pthread_mutex_lock cuando se aplica a un mutex mantenido por el subproceso de llamada. (editó la respuesta en consecuencia)
– Sebastián
8 de noviembre de 2013 a las 16:33
-
Claro, pero incompleto. ¿Cómo se comporta un mutex “temporizado” en esas circunstancias? ¿Existen otras diferencias entre los tipos mutex? ¿Por qué se llama mutex “adaptable” o “rápido” si su característica definitoria es que interbloquea subprocesos en múltiples bloqueos?
– laslowh
8 de noviembre de 2013 a las 16:37
Aquí tienes. Tal como lo leí, es un mutex brutalmente simple al que no le importa nada excepto hacer que el caso de no contención funcione rápido.
-
¿Te importa si combino tu respuesta con la mía? Al menos copiaría y pegaría el párrafo relevante aquí. Gracias por la info!
– Sebastián
15 de noviembre de 2013 a las 19:54
-
@Sebastien feliz de ayudar, adelante. Por lo que puedo decir, aunque no he investigado tanto, el documento será la fuente.
– jthill
15 de noviembre de 2013 a las 21:21
-
@jthill: Buen hallazgo. Es una locura que la única documentación que podemos encontrar colectivamente sea un correo electrónico en la lista del kernel de Linux.
– laslowh
18 de noviembre de 2013 a las 19:53
-
Esto es poco más que una respuesta de solo enlace, me temo.
– Suma
16 sep 2021 a las 17:41