c++ c++11 mutex object-design recursive-mutex

c++ - std:: mutex vs std:: recursive_mutex como miembro de la clase



c++11 object-design (3)

debería std::recursive_mutex ser por defecto y std::mutex considerado como una optimización del rendimiento?

No, realmente no. La ventaja de usar bloqueos no recursivos no es solo una optimización del rendimiento, significa que su código se autoverifica que las operaciones atómicas a nivel de hoja realmente son a nivel de hoja, no están llamando a otra cosa que use el bloqueo.

Hay una situación razonablemente común en la que tiene:

  • una función que implementa alguna operación que necesita ser serializada, por lo que toma el mutex y lo hace.
  • otra función que implementa una operación serializada más grande, y quiere llamar a la primera función para hacer un paso de ella, mientras mantiene el bloqueo para la operación más grande.

En aras de un ejemplo concreto, tal vez la primera función elimine atómicamente un nodo de una lista, mientras que la segunda función elimina atómicamente dos nodos de una lista (y usted nunca desea que otro hilo vea la lista con solo uno de los dos nodos capturados). fuera).

No necesita mutexes recursivos para esto. Por ejemplo, podría refactorizar la primera función como una función pública que toma el bloqueo y llama a una función privada que hace la operación "insegura". La segunda función puede llamar a la misma función privada.

Sin embargo, a veces es conveniente usar un mutex recursivo en su lugar. Todavía hay un problema con este diseño: remove_two_nodes llama a remove_one_node en un punto donde un invariante de clase no se mantiene (la segunda vez que lo llama, la lista está precisamente en el estado que no queremos exponer). Pero suponiendo que sepamos que remove_one_node no se basa en esa invariante, esta no es una falla remove_one_node en el diseño, es solo que hemos hecho que nuestras reglas sean un poco más complejas que las "invariantes de todas las clases" siempre válidas siempre que cualquier función pública es ingresado".

Entonces, el truco es ocasionalmente útil y no odio los mutex recursivos en la medida en que lo hace el artículo. No tengo el conocimiento histórico para argumentar que el motivo de su inclusión en Posix es diferente de lo que dice el artículo, "para demostrar los atributos mutex y extensiones de subprocesos". Sin embargo, ciertamente no los considero los predeterminados.

Creo que es seguro decir que si en su diseño no está seguro de si necesita un bloqueo recursivo o no, entonces su diseño está incompleto. Más tarde se arrepentirá del hecho de que está escribiendo código y no sabe algo tan fundamentalmente importante como si el bloqueo ya está o no retenido. Así que no pongas un candado recursivo "por las dudas".

Si sabes que necesitas uno, usa uno. Si sabe que no necesita uno, el uso de un bloqueo no recursivo no es solo una optimización, sino que también ayuda a imponer una restricción al diseño. Es más útil que el segundo bloqueo falle, que lo consiga y oculte el hecho de que accidentalmente has hecho algo que tu diseño dice que nunca debería suceder. Pero si sigue su diseño y nunca bloquea el mutex, nunca sabrá si es recursivo o no, por lo que un mutex recursivo no es directamente dañino.

Esta analogía podría fallar, pero aquí hay otra forma de verla. Imagine que tiene una opción entre dos tipos de puntero: uno que anula el programa con una pila cuando se desreferencia un puntero nulo y otro que devuelve 0 (o para extenderlo a más tipos: se comporta como si el puntero se refiere a un valor -inicializado objeto). Un mutex no recursivo es un poco como el que aborta, y un mutex recursivo es un poco como el que devuelve 0. Ambos tienen potencialmente su uso: la gente a veces llega hasta cierto punto para implementar un "silencio no-a". valor de "valor". Pero en el caso en que su código esté diseñado para nunca desreferenciar un puntero nulo, no querrá usar de manera predeterminada la versión que permite silenciosamente que eso suceda.

He visto a algunas personas odiar en recursive_mutex :

http://www.zaval.org/resources/library/butenhof1.html

Pero cuando pienso en cómo implementar una clase segura para subprocesos (protegida con mutex), me parece tremendamente difícil probar que todos los métodos que deben estar protegidos con mutex están protegidos contra mutex y que el mutex está bloqueado a lo sumo una vez.

Entonces, para el diseño orientado a objetos, std::recursive_mutex debe ser predeterminado y std::mutex considerado como una optimización del rendimiento en general, a menos que se use solo en un lugar (para proteger solo un recurso).

Para aclarar las cosas, estoy hablando de un mutex privado no estático. Entonces, cada instancia de clase tiene solo un mutex.

Al comienzo de cada método público:

{ std::scoped_lock<std::recursive_mutex> sl;


La mayoría de las veces, si cree que necesita un mutex recursivo, entonces su diseño es incorrecto, por lo que definitivamente no debería ser el predeterminado.

Para una clase con un solo mutex que proteja los miembros de datos, entonces el mutex debería estar bloqueado en todas las funciones miembro public , y todas las funciones miembro private deberían suponer que el mutex ya está bloqueado.

Si una función de miembro public necesita llamar a otra función de miembro public , divida la segunda en dos: una función de implementación private que hace el trabajo, y una función de miembro public que simplemente bloquea el mutex y llama al private . La primera función miembro también puede llamar a la función de implementación sin tener que preocuparse por el bloqueo recursivo.

p.ej

class X { std::mutex m; int data; int const max=50; void increment_data() { if (data >= max) throw std::runtime_error("too big"); ++data; } public: X():data(0){} int fetch_count() { std::lock_guard<std::mutex> guard(m); return data; } void increase_count() { std::lock_guard<std::mutex> guard(m); increment_data(); } int increase_count_and_return() { std::lock_guard<std::mutex> guard(m); increment_data(); return data; } };

Este es, por supuesto, un ejemplo inventado trivial, pero la función increment_data se comparte entre dos funciones miembro públicas, cada una de las cuales bloquea el mutex. En el código de un solo subproceso, podría incluirse en increase_count , y increase_count_and_return podría llamarlo, pero no podemos hacerlo en código multiproceso.

Esta es solo una aplicación de buenos principios de diseño: las funciones de los miembros públicos asumen la responsabilidad de bloquear el mutex y delegar la responsabilidad de hacer el trabajo a la función de miembro privado.

Esto tiene la ventaja de que las funciones de miembros public solo tienen que ver con ser llamadas cuando la clase está en un estado consistente: el mutex está desbloqueado, y una vez que está bloqueado, todas las invariantes se mantienen. Si llama funciones de miembro public entre sí, entonces tienen que manejar el caso de que el mutex ya está bloqueado, y que las invariantes no necesariamente se mantienen.

También significa que cosas como espera de variables de condición funcionarán: si pasas un bloqueo en un mutex recursivo a una variable de condición, entonces (a) necesitas usar std::condition_variable_any porque std::condition_variable no funcionará, y (b) ) solo se libera un nivel de bloqueo, por lo que aún puede mantener el bloqueo y, por lo tanto, un punto muerto porque el hilo que desencadenaría el predicado y notificaría no puede adquirir el bloqueo.

Me cuesta pensar en un escenario donde se requiere un mutex recursivo.


No voy a pesar directamente sobre el debate mutex versus recursive_mutex, pero pensé que sería bueno compartir un escenario donde los recursivos_mutex son absolutamente críticos para el diseño.

Cuando trabajo con Boost :: asio, Boost :: coroutine (y probablemente cosas como NT Fibers aunque estoy menos familiarizado con ellos), es absolutamente esencial que sus mutex sean recursivos incluso sin el problema de diseño de reentrada.

La razón es porque el enfoque basado en coroutine por su propio diseño suspenderá la ejecución dentro de una rutina y luego lo reanudará. Esto significa que dos métodos de nivel superior de una clase podrían "ser llamados al mismo tiempo en el mismo hilo" sin que se realicen sub llamadas.