¿Por qué no puedo verificar si un mutex está bloqueado?

28

C ++ 14 parece haber omitido un mecanismo para verificar si un std::mutexestá bloqueado o no. Vea esta pregunta SO:

https://stackoverflow.com/questions/21892934/how-to-assert-if-a-stdmutex-is-locked

Hay varias formas de evitar esto, por ejemplo, usando;

std::mutex::try_lock()
std::unique_lock::owns_lock()

Pero ninguno de estos son soluciones particularmente satisfactorias.

try_lock()se le permite devolver un falso negativo y tiene un comportamiento indefinido si el hilo actual ha bloqueado el mutex. También tiene efectos secundarios. owns_lock()requiere la construcción de una unique_lockencima del original std::mutex.

Obviamente podría rodar el mío, pero preferiría entender las motivaciones para la interfaz actual.

La capacidad de verificar el estado de un mutex (p std::mutex::is_locked(). Ej. ) No me parece una solicitud esotérica, por lo que sospecho que el Comité Estándar omitió deliberadamente esta característica en lugar de ser un descuido.

¿Por qué?

Editar: Ok, entonces tal vez este caso de uso no sea tan común como esperaba, así que ilustraré mi escenario particular. Tengo un algoritmo de aprendizaje automático que se distribuye en múltiples hilos. Cada subproceso funciona de forma asíncrona y vuelve a un grupo maestro una vez que ha completado un problema de optimización.

Luego bloquea un mutex maestro. Luego, el subproceso debe elegir un nuevo padre desde el cual mutar una descendencia, pero solo puede elegir entre los padres que actualmente no tienen descendencia que están siendo optimizados por otros subprocesos. Por lo tanto, necesito realizar una búsqueda para encontrar padres que actualmente no están bloqueados por otro hilo. No hay riesgo de que el estado del mutex cambie durante la búsqueda, ya que el mutex del hilo maestro está bloqueado. Obviamente hay otras soluciones (actualmente estoy usando un indicador booleano) pero pensé que el mutex ofrece una solución lógica a este problema, ya que existe con el propósito de sincronización entre subprocesos.

cuant
fuente
42
Realmente no puede verificar razonablemente si un mutex está bloqueado, porque un nanosegundo después de la verificación puede desbloquearse o bloquearse. Entonces, si escribió "if (mutex_is_locked ()) ...", mutex_is_locked podría devolver el resultado correcto, pero para el momento en que se ejecuta "if", es incorrecto.
gnasher729
1
Este ^. ¿Qué información útil esperas obtener is_locked?
Inútil
3
Esto se siente como un problema XY. ¿Por qué intenta evitar la reutilización de los padres solo mientras se genera un hijo? ¿Tiene el requisito de que cualquier padre solo pueda tener exactamente una descendencia? Su cerradura no impedirá eso. ¿No tienes generaciones claras? Si no es así, ¿sabe que las personas que pueden optimizarse más rápido tienen una mayor aptitud, ya que pueden seleccionarse con mayor frecuencia / antes? Si usa generaciones, ¿por qué no selecciona a todos los padres por adelantado y luego deja que los hilos recuperen a los padres de una cola? ¿Generar descendencia es realmente tan costoso que necesita múltiples hilos?
amon
10
@quant: no veo por qué sus mutexes de objeto primario en su aplicación de ejemplo deben ser mutexes en absoluto: si tiene un mutex maestro que se bloquea cada vez que se configuran, simplemente puede usar una variable booleana para indicar su estado.
Periata Breatta
44
No estoy de acuerdo con la última oración de la pregunta. Un valor booleano simple es mucho más limpio que un mutex aquí. Conviértalo en un bool atómico si no desea bloquear el mutex maestro para "devolver" un padre.
Sebastian Redl

Respuestas:

53

Puedo ver al menos dos problemas graves con la operación sugerida.

El primero ya fue mencionado en un comentario de @ gnasher729 :

Realmente no puede verificar razonablemente si un mutex está bloqueado, porque un nanosegundo después de la verificación puede desbloquearse o bloquearse. Entonces, si escribió if (mutex_is_locked ()) …, mutex_is_lockedpodría devolver el resultado correcto, pero para cuando ifse ejecute, está mal.

La única manera de asegurarse de que la propiedad "está bloqueada actualmente" de un mutex no cambia es, bueno, bloquearla usted mismo.

El segundo problema que veo es que, a menos que bloquee un mutex, su hilo no se sincroniza con el hilo que previamente había bloqueado el mutex. Por lo tanto, ni siquiera está bien definido hablar sobre "antes" y "después" y si el mutex está bloqueado o no es como preguntar si el gato de Schrödiger está vivo sin intentar abrir la caja.

Si entiendo correctamente, ambos problemas serían discutibles en su caso particular gracias a que el mutex maestro está bloqueado. Pero este no parece ser un caso particularmente común para mí, así que creo que el comité hizo lo correcto al no agregar una función que podría ser algo útil en escenarios muy especiales y causar daños en todos los demás. (En el espíritu de: "Hacer que las interfaces sean fáciles de usar correctamente y difíciles de usar incorrectamente")

Y si puedo decir, creo que la configuración que tiene actualmente no es la más elegante y podría ser refactorizada para evitar el problema por completo. Por ejemplo, en lugar de que el hilo maestro verifique a todos los padres potenciales por uno que no esté bloqueado actualmente, ¿por qué no mantener una cola de padres listos? Si un hilo quiere optimizar otro, saca el siguiente de la cola y, tan pronto como tiene nuevos padres, los agrega a la cola. De esa manera, ni siquiera necesita el hilo maestro como coordinador.

5gon12eder
fuente
Gracias, esta es una buena respuesta. La razón por la que no quiero mantener una cola de padres listos es porque necesito preservar el orden en que fueron creados los padres (ya que esto dicta su vida útil). Esto se hace fácilmente con una cola LIFO. Si empiezo a tirar de cosas dentro y fuera, tendría que mantener un mecanismo de pedido separado que complicaría las cosas, de ahí el enfoque actual.
Quant
14
@quant: Si tiene dos propósitos para poner en cola a los padres, puede hacerlo con dos colas ...
@quant: está eliminando un elemento (como máximo) una vez, pero presumiblemente está procesando cada una de las veces, por lo que está optimizando el caso raro a expensas del caso común. Esto rara vez es deseable.
Jerry Coffin
2
Pero es razonable preguntar si el hilo actual ha bloqueado el mutex.
Expiación limitada
@LimitedAtonement No realmente. Para hacer esto, mutex tiene que almacenar información adicional (id de hilo), haciéndolo más lento. Los mutex recursivos ya hacen esto, deberías hacerlo en su lugar.
StaceyGirl
9

Parece que está utilizando los mutexes secundarios para no bloquear el acceso a un problema de optimización, sino para determinar si un problema de optimización se está optimizando en este momento o no.

Eso es completamente innecesario. Tendría una lista de problemas que necesitan optimización, una lista de problemas que se están optimizando en este momento y una lista de problemas que se han optimizado. (No tome "lista" literalmente, suponga que significa "cualquier estructura de datos apropiada").

Las operaciones de agregar un nuevo problema a la lista de problemas no optimizados, o mover un problema de una lista a la siguiente, se realizarían bajo la protección del único mutex "maestro".

gnasher729
fuente
1
¿No crees que un objeto de tipo std::mutexes apropiado para dicha estructura de datos?
Quant
2
@quant - no. std::mutexse basa en una implementación de mutex definida por el sistema operativo que bien puede tomar recursos (por ejemplo, identificadores) que son limitados y lentos para asignar y / o operar. Es probable que el uso de un único mutex para bloquear el acceso a una estructura de datos interna sea mucho más eficiente y posiblemente también más escalable.
Periata Breatta
1
Considere también las variables de condición. Pueden hacer que muchas estructuras de datos como esta sean realmente fáciles.
Cort Ammon - Restablece a Monica el
2

Como otros dijeron, no hay ningún caso de uso en is_lockedel que un mutex sea beneficioso, por eso la función no existe.

El caso con el que tiene un problema es increíblemente común, es básicamente lo que hacen los subprocesos de trabajo, que son una de las implementaciones de subprocesos , si no la más común.

Tienes un estante con 10 cajas. Tienes 4 trabajadores trabajando con estas cajas. ¿Cómo se asegura de que los 4 trabajadores trabajen en diferentes cajas? El primer trabajador saca una caja del estante antes de comenzar a trabajar en ella. El segundo trabajador ve 9 cajas en el estante.

No hay mutexes para bloquear las cajas, por lo que no es necesario ver el estado del mutex imaginario en la caja, y abusar de un mutex como booleano es simplemente incorrecto. El mutex bloquea el estante.

Peter
fuente
1

Además de las dos razones dadas en la respuesta anterior de 5gon12eder, me gustaría agregar que no es necesario ni deseable.

Si ya tiene un mutex, ¡es mejor que sepa que lo tiene! No necesitas preguntar. Al igual que con poseer un bloque de memoria o cualquier otro recurso, debe saber exactamente si lo posee o no, y cuándo es apropiado liberar / eliminar el recurso.
Si ese no es el caso, su programa está mal diseñado y se dirige a problemas.

Si necesita acceder al recurso compartido protegido por el mutex, y aún no tiene el mutex, entonces necesita adquirir el mutex. No hay otra opción, de lo contrario, la lógica de su programa no es correcta.
Es posible que el bloqueo sea aceptable o inaceptable, en cualquier caso, lock()o try_lock()le dará el comportamiento que desea. Todo lo que necesita saber, positivamente y sin duda, es si adquirió con éxito el mutex (el valor de retorno de try_lockle dice). Es intrascendente si alguien más lo tiene o si tiene una falla espuria.

En cualquier otro caso, sin rodeos, no es asunto tuyo. No necesita saber, y no debe saber, ni hacer suposiciones (por los problemas de sincronización y puntualidad mencionados en la otra pregunta).

Damon
fuente
1
¿Qué sucede si deseo realizar una operación de clasificación en los recursos que están actualmente disponibles para el bloqueo?
cuant
Pero, ¿es algo realista que suceda? Lo consideraría bastante inusual. Yo diría que cualquiera de los recursos ya tiene algún tipo de clasificación intrínseca, entonces primero deberá hacer (adquirir el bloqueo para) el más importante. Ejemplo: debe actualizar la simulación física antes de la representación. O bien, la clasificación es más o menos deliberada, entonces también puedes usar try_lockel primer recurso y, si ese falla, prueba el segundo. Ejemplo: Tres conexiones agrupadas persistentes al servidor de la base de datos, y debe usar una para enviar un comando.
Damon
44
@quant - "una operación de clasificación de los recursos que están actualmente disponibles para el bloqueo" - en general, hacer este tipo de cosas es una forma realmente fácil y rápida de escribir código que se interrumpe de una manera que te cuesta descubrir. La adquisición de cerraduras y la liberación determinista es, en casi todos los casos, la mejor política. Buscar un bloqueo basado en un criterio que podría cambiar es pedir problemas.
Periata Breatta
@PeriataBreatta Mi programa es intencionalmente indeterminado. Ahora veo que este atributo no es común, por lo que puedo entender la omisión de características como is_locked()esa que podrían facilitar ese comportamiento.
cuant
La clasificación y el bloqueo de @quant son problemas completamente separados. Si de alguna manera desea ordenar o reordenar una cola con un candado, bloquéelo, ordénelo y luego desbloquéelo. Si lo necesita is_locked, existe una solución mucho mejor para su problema que la que tiene en mente.
Peter
1

Es posible que desee utilizar atomic_flag con el orden de memoria predeterminado. No tiene carreras de datos y nunca arroja excepciones como mutex con múltiples llamadas de desbloqueo (y aborta sin control, podría agregar ...). Alternativamente, hay atómico (por ejemplo, atómico [bool] o atómico [int] (con corchetes triangulares, no [])), que tiene buenas funciones como load y compare_exchange_strong.

Andrés
fuente
1

Quiero agregar un caso de uso para esto: habilitaría una función interna para garantizar como condición previa / afirmación que la persona que llama realmente mantiene el bloqueo.

Para las clases con varias de estas funciones internas, y posiblemente muchas funciones públicas que las llamen, podría garantizar que alguien que agregue otra función pública que llame a la interna efectivamente adquiera el bloqueo.

class SynchronizedClass
{

public:

void publicFunc()
{
  std::lock_guard<std::mutex>(_mutex);

  internalFuncA();
}

// A lot of code

void newPublicFunc()
{
  internalFuncA(); // whops, forgot to acquire the lock
}


private:

void internalFuncA()
{
  assert(_mutex.is_locked_by_this_thread());

  doStuffWithLockedResource();
}

};
B3ret
fuente