c++ - sistemas - que es un deadlock en base de datos
Estrategias y técnicas de bloqueo para evitar interbloqueos en el código (5)
Aunque no es una alternativa a la solución de secuencia conocida que menciona, Andrei Alexandrescu escribió sobre algunas técnicas para verificar el tiempo de compilación, que la adquisición de bloqueos se realiza a través de los mecanismos previstos. Ver http://www.informit.com/articles/article.aspx?p=25298
La solución común para evitar el interbloqueo en el código es asegurarse de que la secuencia de bloqueo se produzca de forma común independientemente de qué subproceso acceda a los recursos.
Por ejemplo, con los subprocesos T1 y T2, donde T1 accede al recurso A y luego B y T2 accede al recurso B y luego A. Al bloquear los recursos en el orden en que se necesitan, se produce un bloqueo muerto. La solución simple es bloquear A y luego bloquear B, independientemente de la secuencia específica de la orden usará los recursos.
Situación problemática:
Thread1 Thread2
------- -------
Lock Resource A Lock Resource B
Do Resource A thing... Do Resource B thing...
Lock Resource B Lock Resource A
Do Resource B thing... Do Resource A thing...
Solución posible:
Thread1 Thread2
------- -------
Lock Resource A Lock Resource A
Lock Resource B Lock Resource B
Do Resource A thing... Do Resource B thing...
Do Resource B thing... Do Resource A thing...
Mi pregunta es ¿qué otras técnicas, patrones o prácticas comunes se utilizan en la codificación para garantizar la prevención de bloqueo muerto?
El ordenamiento consistente del bloqueo es prácticamente la primera y la última palabra cuando se trata de evitar el interbloqueo.
Existen técnicas relacionadas, como la programación sin bloqueos (donde ningún hilo espera un candado, y por lo tanto no hay posibilidad de un ciclo), pero ese es realmente un caso especial de la regla "evitar orden de bloqueo incoherente", es decir, evite el bloqueo inconsistente evitando todo bloqueo. Desafortunadamente, la programación sin bloqueo tiene sus propios problemas, por lo que tampoco es una panacea.
Si desea ampliar el alcance un poco, existen métodos para detectar interbloqueos cuando ocurren (si por alguna razón no puede diseñar su programa para evitarlos) y formas de romper interbloqueos cuando ocurren (p. Ej., Siempre bloquearse con un tiempo de espera excedido o forzar a uno de los subprocesos bloqueados a que su comando Lock () falle, o incluso al matar uno de los subprocesos bloqueados); pero creo que todos son bastante inferiores a simplemente asegurarse de que no se produzcan bloqueos en primer lugar.
(Por cierto, si desea una forma automática de verificar si su programa tiene puntos muertos potenciales en él, consulte la herramienta helgrind de valgrind. Controlará los patrones de bloqueo de su código y le notificará cualquier incoherencia, muy útil)
La técnica que describes no es solo común: es una técnica que se ha demostrado que funciona todo el tiempo. Sin embargo, hay algunas otras reglas que debe seguir al codificar el código en C ++, entre las cuales las más importantes pueden ser:
- no mantenga presionado un candado cuando llame a una función virtual : incluso si en el momento de escribir su código sabe qué función se llamará y qué hará, el código evoluciona y las funciones virtuales quedan anuladas, por lo que en última instancia , no sabrá qué es lo que hace y si le tomará otros bloqueos, lo que significa que perderá su orden de bloqueo garantizada
- Tenga cuidado con las condiciones de carrera : en C ++, nada le dirá cuándo se comparte una determinada porción de datos entre hilos y no utiliza algún tipo de sincronización en ella. Un ejemplo de esto fue publicado en el chat de C ++ Lounge en SO hace unos días, por Luc, como un ejemplo de esto (código al final de esta publicación): simplemente tratando de sincronizar en otra cosa que está en el vecindario no significa que su código esté sincronizado correctamente.
- Trate de ocultar el comportamiento asincrónico : generalmente es mejor ocultar su concurrencia en la arquitectura de su software, de modo que a la mayoría de los códigos de llamadas no le importe si hay un hilo allí o no. Hace que la arquitectura sea más fácil de trabajar, especialmente para alguien que no está acostumbrado a la concurrencia.
Podría continuar por un tiempo, pero en mi experiencia, la forma más fácil de trabajar con hilos es utilizar patrones que son bien conocidos por todos los que podrían trabajar con el código, como el patrón productor / consumidor: es fácil de explicar y solo necesita una herramienta (una cola) para permitir que sus hilos se comuniquen entre sí. Después de todo, la única razón por la cual dos hilos se sincronizan entre sí es permitirles comunicarse.
Consejos más generales:
- No intentes con la programación sin bloqueos hasta que tengas experiencia con la programación concurrente usando bloqueos: es una forma fácil de despertarte o de encontrar errores muy extraños.
- Reduzca el número de variables compartidas y la cantidad de veces que se accede a esas variables a un mínimo.
- No cuente con dos eventos que siempre se producen en el mismo orden, incluso si no puede ver ninguna forma de invertir el orden.
- De manera más general: no cuente con el tiempo: no crea que una tarea determinada siempre debe tomar una cantidad determinada de tiempo.
El siguiente código fallará:
#include <thread>
#include <cassert>
#include <chrono>
#include <iostream>
#include <mutex>
void
nothing_could_possibly_go_wrong()
{
int flag = 0;
std::condition_variable cond;
std::mutex mutex;
int done = 0;
typedef std::unique_lock<std::mutex> lock;
auto const f = [&]
{
if(flag == 0) ++flag;
lock l(mutex);
++done;
cond.notify_one();
};
std::thread threads[2] = {
std::thread(f),
std::thread(f)
};
threads[0].join();
threads[1].join();
lock l(mutex);
cond.wait(l, [done] { return done == 2; });
// surely this can''t fail!
assert( flag == 1 );
}
int
main()
{
for(;;) nothing_could_possibly_go_wrong();
}
Otra técnica es la programación transaccional. Sin embargo, esto no es muy común, ya que generalmente involucra hardware especializado (la mayoría actualmente solo en instituciones de investigación).
Cada recurso realiza un seguimiento de las modificaciones de diferentes hilos. El primer hilo para confirmar los cambios en todos los recursos (está utilizando) gana el resto del hilo (usando esos recursos) se deshace para volver a intentarlo con los recursos en el nuevo estado comprometido.
Un punto de partida simplista para leer sobre el tema es la memoria transaccional .
Usted está preguntando sobre el nivel de diseño, pero agregaré algunas prácticas de programación de nivel inferior.
- Clasifica cada función (método) como bloqueante , no bloqueante o con un comportamiento de bloqueo desconocido.
- Una función de bloqueo es una función que adquiere un bloqueo o llama a una llamada lenta del sistema (que en la práctica significa que tiene E / S) o llama a una función de bloqueo .
- Si una función está garantizada como no bloqueante es parte de la especificación de esa función, al igual que sus precondiciones y su grado de seguridad de excepción. Por lo tanto, debe documentarse como tal. En Java, uso una anotación; en C ++ documentado usando Doxygen usaría una frase forumalic en el comentario del encabezado de la función.
- Considere la posibilidad de llamar a una función que no está especificada como no bloqueante mientras mantiene un bloqueo como peligroso.
- Refactorice dicho código peligroso para eliminar el peligro o para concentrar el peligro en una pequeña sección de código (quizás dentro de su propia función).
- Para el código peligroso restante, proporcione una prueba informal de que el código no es realmente peligroso en el comentario del código.