¿Cuál es la diferencia entre deadlock y livelock?

Respuestas:

398

Tomado de http://en.wikipedia.org/wiki/Deadlock :

En la computación concurrente, un punto muerto es un estado en el que cada miembro de un grupo de acciones espera que algún otro miembro libere un bloqueo

Un livelock es similar a un punto muerto, excepto que los estados de los procesos involucrados en el livelock cambian constantemente entre sí, ninguno progresa. Livelock es un caso especial de inanición de recursos; la definición general solo establece que un proceso específico no está progresando.

Un ejemplo del mundo real de livelock ocurre cuando dos personas se encuentran en un pasillo estrecho, y cada una trata de ser cortés moviéndose a un lado para dejar pasar a la otra, pero terminan balanceándose de lado a lado sin progresar porque ambas se mueven repetidamente de la misma manera al mismo tiempo.

Livelock es un riesgo con algunos algoritmos que detectan y se recuperan del punto muerto. Si más de un proceso toma medidas, el algoritmo de detección de punto muerto se puede activar repetidamente. Esto puede evitarse asegurando que solo un proceso (elegido al azar o por prioridad) tome medidas.

mah
fuente
8
Ya lo encontré, pero no tienen ejemplos allí como se puede ver, gracias de todos modos
macindows
61
No proporcionaré un ejemplo de código, pero considere dos procesos, cada uno esperando un recurso que el otro tiene pero esperando de manera no bloqueante. Cuando cada uno aprende que no puede continuar, libera su recurso retenido y duerme durante 30 segundos, luego recupera su recurso original seguido de intentar el recurso que el otro proceso retuvo, luego se fue y luego volvió a adquirir. Dado que ambos procesos están tratando de hacer frente (solo mal), este es un livelock.
mah
44
¿Me puede dar el mismo ejemplo pero con punto muerto, gracias de antemano
macindows
32
Un ejemplo de punto muerto es mucho más fácil ... suponga dos procesos A y B, y cada uno quiere el recurso r1 y el recurso r2. Suponga que A recibe (o ya tiene) r1, y B recibe (o ya tiene) r2. Ahora cada uno intenta obtener el recurso que tiene el otro, sin ningún tiempo de espera. A está bloqueado porque B tiene r2, y B está bloqueado porque A tiene r1. Cada proceso está bloqueado y, por lo tanto, no puede liberar el recurso que el otro quiere, causando un punto muerto.
mah
2
Dentro del contexto de la memoria transaccional, hay un gran video que muestra un punto muerto y un bloqueo en vivo: youtube.com/watch?v=_IxsOEEzf-c
BlackVegetable
78

Livelock

Un hilo a menudo actúa en respuesta a la acción de otro hilo. Si la acción del otro subproceso también es una respuesta a la acción de otro subproceso, puede resultar en livelock.

Al igual que con el punto muerto, los subprocesos en vivo no pueden avanzar más . Sin embargo, los hilos no están bloqueados , simplemente están demasiado ocupados respondiéndose unos a otros para reanudar el trabajo . Esto es comparable a dos personas que intentan cruzarse en un corredor: Alphonse se mueve a su izquierda para dejar pasar a Gaston, mientras que Gaston se mueve a su derecha para dejar pasar a Alphonse. Al ver que todavía se están bloqueando, Alphonse se mueve hacia su derecha, mientras que Gastón se mueve hacia su izquierda. Todavía se están bloqueando, y así sucesivamente ...

La principal diferencia entre livelock y deadlock es que los hilos no se bloquearán, sino que intentarán responderse entre sí de manera continua.

En esta imagen, ambos círculos (hilos o procesos) intentarán dar espacio al otro moviéndose hacia la izquierda y hacia la derecha. Pero no pueden avanzar más.

ingrese la descripción de la imagen aquí

Buru
fuente
Ejemplos de código para livelocks stackoverflow.com/questions/1036364/good-example-of-livelock
Yauhen Yakimovich
1
Esta cosa tiene un nombre. Una palabra de argot tal vez, pero aún así: schlumperdink : P
John Red
64

Todo el contenido y ejemplos aquí son de

Sistemas operativos: principios internos y principios de diseño
William Stallings
8º Edición

Punto muerto : una situación en la que dos o más procesos no pueden continuar porque cada uno está esperando que el otro haga algo.

Por ejemplo, considere dos procesos, P1 y P2, y dos recursos, R1 y R2. Suponga que cada proceso necesita acceso a ambos recursos para realizar parte de su función. Entonces es posible tener la siguiente situación: el sistema operativo asigna R1 a P2 y R2 a P1. Cada proceso está esperando uno de los dos recursos. Ninguno de los dos liberará el recurso que ya posee hasta que haya adquirido el otro recurso y haya realizado la función que requiere ambos recursos. Los dos procesos están estancados

Livelock : una situación en la que dos o más procesos cambian continuamente sus estados en respuesta a cambios en los otros procesos sin realizar ningún trabajo útil:

Inanición : una situación en la que el planificador pasa por alto indefinidamente un proceso ejecutable; aunque puede continuar, nunca se elige.

Suponga que tres procesos (P1, P2, P3) requieren acceso periódico al recurso R. Considere la situación en la que P1 está en posesión del recurso, y tanto P2 como P3 están retrasados, esperando ese recurso. Cuando P1 sale de su sección crítica, P2 o P3 deberían tener acceso a R. Suponga que el sistema operativo otorga acceso a P3 y que P1 nuevamente requiere acceso antes de que P3 complete su sección crítica. Si el sistema operativo concede acceso a P1 después de que P3 haya finalizado y, posteriormente, concede acceso alternativamente a P1 y P3, entonces a P2 se le puede denegar indefinidamente el acceso al recurso, aunque no haya una situación de punto muerto.

APÉNDICE A - TEMAS EN CONCURRENCIA

Ejemplo de punto muerto

Si ambos procesos establecen sus banderas en verdadero antes de que cualquiera haya ejecutado la instrucción while, entonces cada uno pensará que el otro ha entrado en su sección crítica, causando un punto muerto.

/* PROCESS 0 */
flag[0] = true;            // <- get lock 0
while (flag[1])            // <- is lock 1 free?
    /* do nothing */;      // <- no? so I wait 1 second, for example
                           // and test again.
                           // on more sophisticated setups we can ask
                           // to be woken when lock 1 is freed
/* critical section*/;     // <- do what we need (this will never happen)
flag[0] = false;           // <- releasing our lock

 /* PROCESS 1 */
flag[1] = true;
while (flag[0])
    /* do nothing */;
/* critical section*/;
flag[1] = false;

Ejemplo de Livelock

/* PROCESS 0 */
flag[0] = true;          // <- get lock 0
while (flag[1]){         
    flag[0] = false;     // <- instead of sleeping, we do useless work
                         //    needed by the lock mechanism
    /*delay */;          // <- wait for a second
    flag[0] = true;      // <- and restart useless work again.
}
/*critical section*/;    // <- do what we need (this will never happen)
flag[0] = false; 

/* PROCESS 1 */
flag[1] = true;
while (flag[0]) {
    flag[1] = false;
    /*delay */;
    flag[1] = true;
}
/* critical section*/;
flag[1] = false;

[...] considere la siguiente secuencia de eventos:

  • P0 establece el indicador [0] en verdadero.
  • P1 establece el indicador [1] en verdadero.
  • P0 comprueba la bandera [1].
  • P1 comprueba la bandera [0].
  • P0 establece el indicador [0] en falso.
  • P1 establece el indicador [1] en falso.
  • P0 establece el indicador [0] en verdadero.
  • P1 establece el indicador [1] en verdadero.

Esta secuencia podría extenderse indefinidamente, y ninguno de los procesos podría entrar en su sección crítica. Estrictamente hablando, esto no es un punto muerto , porque cualquier alteración en la velocidad relativa de los dos procesos romperá este ciclo y permitirá que uno entre en la sección crítica. Esta condición se conoce como livelock . Recuerde que el punto muerto se produce cuando un conjunto de procesos desea ingresar a sus secciones críticas pero ningún proceso puede tener éxito. Con livelock , hay posibles secuencias de ejecuciones que tienen éxito, pero también es posible describir una o más secuencias de ejecución en las que ningún proceso ingresa a su sección crítica.

Ya no es contenido del libro.

¿Y qué hay de los spinlocks?

Spinlock es una técnica para evitar el costo del mecanismo de bloqueo del sistema operativo. Por lo general, harías:

try
{
   lock = beginLock();
   doSomething();
}
finally
{
   endLock();
}

Un problema comienza a aparecer cuando beginLock()cuesta mucho más que doSomething(). En términos muy exagerados, imagine lo que sucede cuando beginLockcuesta 1 segundo, pero doSomethingcuesta solo 1 milisegundo.

En este caso, si esperaba 1 milisegundo, evitaría verse obstaculizado durante 1 segundo.

¿Por qué beginLockcostaría tanto? Si el bloqueo es gratuito, no cuesta mucho (consulte https://stackoverflow.com/a/49712993/5397116 ), pero si el bloqueo no es gratuito, el sistema operativo "congelará" su hilo, configure un mecanismo para despertarlo cuando se libera la cerradura y luego te despierta nuevamente en el futuro.

Todo esto es mucho más costoso que algunos bucles que controlan la cerradura. Es por eso que a veces es mejor hacer un "spinlock".

Por ejemplo:

void beginSpinLock(lock)
{
   if(lock) loopFor(1 milliseconds);
   else 
   {
     lock = true;
     return;
   }

   if(lock) loopFor(2 milliseconds);
   else 
   {
     lock = true;
     return;
   }

   // important is that the part above never 
   // cause the thread to sleep.
   // It is "burning" the time slice of this thread.
   // Hopefully for good.

   // some implementations fallback to OS lock mechanism
   // after a few tries
   if(lock) return beginLock(lock);
   else 
   {
     lock = true;
     return;
   }
}

Si su implementación no es cuidadosa, puede caer en livelock, gastando toda la CPU en el mecanismo de bloqueo.

Ver también:

https://preshing.com/20120226/roll-your-own-lightweight-mutex/
¿Es correcta y óptima mi implementación de bloqueo de giro?

Resumen :

Punto muerto : situación en la que nadie progresa, no hace nada (dormir, esperar, etc.). El uso de la CPU será bajo;

Livelock : situación en la que nadie progresa, pero la CPU se gasta en el mecanismo de bloqueo y no en su cálculo;

Inanición: situación en la que un procress nunca tiene la oportunidad de correr; por pura mala suerte o por alguna de sus propiedades (baja prioridad, por ejemplo);

Spinlock : técnica para evitar el costo esperando que se libere el bloqueo.

Daniel Frederico Lins Leite
fuente
Señor, el ejemplo que dio para Deadlock es en realidad un ejemplo de Spinlock. El punto muerto se produce cuando se bloquea un conjunto de procesos que no están listos o en ejecución y esperando algunos recursos. Pero en nuestro ejemplo, cada uno está realizando alguna tarea, es decir, verificando la condición una y otra vez. Corrígeme si estoy equivocado.
Vinay Yadav
El ejemplo es tan mínimo que abre posibilidades para esta interpretación, por lo que lo mejoré para ser un poco más explícito sobre su diferencia. Espero que ayude.
Daniel Frederico Lins Leite
Gracias por agregar sobre los spinlocks, de acuerdo a ustedes los spinlocks son una técnica y ustedes también lo justificaron y lo entendí. Pero, ¿qué pasa con ese problema de inversión de prioridad cuando un proceso P1 está en la Sección Crítica y otro proceso de alta prioridad P2 se programa para adelantar a P1 y luego en este caso la CPU está con P2 y nuestro mecanismo de sincronización está con P1. Esto se llama Spinlock ya que P1 está en estado listo y P2 está en estado de ejecución . Aquí el spinlock es un problema. ¿Estoy haciendo las cosas bien? No puedo entender bien las complejidades. Por favor ayuda
Vinay Yadav
Mi sugerencia para usted es crear otra pregunta que indique su problema más claramente. Ahora, si está en "espacio de usuario", y P1 está dentro de una sesión crítica protegida con un SpinLock implementado con un bucle infinito y su preferencia; entonces P2 intentará ingresarlo, fallará y quemará todo su intervalo de tiempo. Creó un livelock (una CPU estará al 100%). (un mal uso sería proteger una sincronización de E / S con spinlock. Puede probar este ejemplo fácilmente) En el "espacio del kernel" tal vez esta nota pueda ayudarlo: lxr.linux.no/linux+v3.6.6/Documentation/…
Daniel Frederico Lins Leite
Muchas gracias por la aclaración. De todos modos, su respuesta fue bastante descriptiva y útil a diferencia de otros
Vinay Yadav
13

DEADLOCK Deadlock es una condición en la que una tarea espera indefinidamente condiciones que nunca pueden satisfacerse: la tarea reclama el control exclusivo sobre los recursos compartidos - la tarea retiene recursos mientras espera que se liberen otros recursos - las tareas no pueden ser forzadas a reponer los recursos - una espera circular condición existe

Las condiciones de Livelock de LIVELOCK pueden surgir cuando dos o más tareas dependen y usan algún recurso que causa una condición de dependencia circular donde esas tareas continúan ejecutándose para siempre, bloqueando así todas las tareas de menor nivel de prioridad (estas tareas de menor prioridad experimentan una condición llamada inanición)

Deepak Lamichhane
fuente
Si las tareas 'livelocked' están siguiendo protocolos de arbitraje de recursos que incluyen demoras de 'retroceso' y, como resultado, pasan la mayor parte de su tiempo en estado de suspensión, entonces otras tareas no se quedarán sin hambre.
Greggo
8

Quizás estos dos ejemplos ilustran la diferencia entre un punto muerto y un punto muerto:


Ejemplo Java para un punto muerto:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class DeadlockSample {

    private static final Lock lock1 = new ReentrantLock(true);
    private static final Lock lock2 = new ReentrantLock(true);

    public static void main(String[] args) {
        Thread threadA = new Thread(DeadlockSample::doA,"Thread A");
        Thread threadB = new Thread(DeadlockSample::doB,"Thread B");
        threadA.start();
        threadB.start();
    }

    public static void doA() {
        System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
        lock1.lock();
        System.out.println(Thread.currentThread().getName() + " : holds lock 1");

        try {
            System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
            lock2.lock();
            System.out.println(Thread.currentThread().getName() + " : holds lock 2");

            try {
                System.out.println(Thread.currentThread().getName() + " : critical section of doA()");
            } finally {
                lock2.unlock();
                System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
            }
        } finally {
            lock1.unlock();
            System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
        }
    }

    public static void doB() {
        System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
        lock2.lock();
        System.out.println(Thread.currentThread().getName() + " : holds lock 2");

        try {
            System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
            lock1.lock();
            System.out.println(Thread.currentThread().getName() + " : holds lock 1");

            try {
                System.out.println(Thread.currentThread().getName() + " : critical section of doB()");
            } finally {
                lock1.unlock();
                System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
            }
        } finally {
            lock2.unlock();
            System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
        }
    }
}

Salida de muestra:

Thread A : waits for lock 1
Thread B : waits for lock 2
Thread A : holds lock 1
Thread B : holds lock 2
Thread B : waits for lock 1
Thread A : waits for lock 2

Ejemplo Java para un livelock:


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LivelockSample {

    private static final Lock lock1 = new ReentrantLock(true);
    private static final Lock lock2 = new ReentrantLock(true);

    public static void main(String[] args) {
        Thread threadA = new Thread(LivelockSample::doA, "Thread A");
        Thread threadB = new Thread(LivelockSample::doB, "Thread B");
        threadA.start();
        threadB.start();
    }

    public static void doA() {
        try {
            while (!lock1.tryLock()) {
                System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
                Thread.sleep(100);
            }
            System.out.println(Thread.currentThread().getName() + " : holds lock 1");

            try {
                while (!lock2.tryLock()) {
                    System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
                    Thread.sleep(100);
                }
                System.out.println(Thread.currentThread().getName() + " : holds lock 2");

                try {
                    System.out.println(Thread.currentThread().getName() + " : critical section of doA()");
                } finally {
                    lock2.unlock();
                    System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
                }
            } finally {
                lock1.unlock();
                System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
            }
        } catch (InterruptedException e) {
            // can be ignored here for this sample
        }
    }

    public static void doB() {
        try {
            while (!lock2.tryLock()) {
                System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
                Thread.sleep(100);
            }
            System.out.println(Thread.currentThread().getName() + " : holds lock 2");

            try {
                while (!lock1.tryLock()) {
                    System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
                    Thread.sleep(100);
                }
                System.out.println(Thread.currentThread().getName() + " : holds lock 1");

                try {
                    System.out.println(Thread.currentThread().getName() + " : critical section of doB()");
                } finally {
                    lock1.unlock();
                    System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
                }
            } finally {
                lock2.unlock();
                System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
            }
        } catch (InterruptedException e) {
            // can be ignored here for this sample
        }
    }
}

Salida de muestra:

Thread B : holds lock 2
Thread A : holds lock 1
Thread A : waits for lock 2
Thread B : waits for lock 1
Thread B : waits for lock 1
Thread A : waits for lock 2
Thread A : waits for lock 2
Thread B : waits for lock 1
Thread B : waits for lock 1
Thread A : waits for lock 2
Thread A : waits for lock 2
Thread B : waits for lock 1
...

Ambos ejemplos obligan a los hilos a adquirir las cerraduras en diferentes órdenes. Mientras que el punto muerto espera la otra cerradura, el livelock no espera realmente, intenta desesperadamente adquirir la cerradura sin la posibilidad de obtenerla. Cada intento consume ciclos de CPU.

mmirwaldt
fuente
El código es lindo. Pero el ejemplo de live-lock no es bueno. Si un subproceso está bloqueado en un valor o está sondeando un cambio de valor no es conceptualmente diferente. Un cambio fácil para ilustrar mejor un bloqueo en vivo es hacer que los hilos A y B liberen los bloqueos que tienen cuando se dan cuenta de que no pueden obtener el segundo bloqueo que necesitan. Luego duermen por un segundo cada uno, recuperan la cerradura que tenían originalmente, luego duermen por otro segundo e intentan adquirir la otra cerradura nuevamente. Entonces el ciclo para cada uno sería: 1) adquirir-mina, 2) dormir, 3) tratar de adquirir otro y fallar, 4) liberar-mina, 5) dormir, 6) Repetir.
CognizantApe
1
Dudo si las cerraduras en vivo que piensas realmente existen el tiempo suficiente como para causar problemas. Cuando siempre renuncia a todos los bloqueos que tiene cuando no puede asignar el siguiente bloqueo, la condición de bloqueo (y bloqueo en vivo) "mantener y esperar" falta porque en realidad ya no hay espera. ( en.wikipedia.org/wiki/Deadlock )
mmirwaldt
de hecho, falta la condición de bloqueo muerto porque estamos hablando de bloqueos activos. El ejemplo que di es similar al ejemplo de pasillo estándar dado: geeksforgeeks.org/deadlock-starvation-and-livelock , en.wikibooks.org/wiki/Operating_System_Design/Concurrency/… , docs.oracle.com/javase/tutorial/essential / concurrencia / ...
CognizantApe
0

Imagine que tiene el hilo A y el hilo B. Ambos están synchroniseden el mismo objeto y dentro de este bloque hay una variable global que ambos están actualizando;

static boolean commonVar = false;
Object lock = new Object;

...

void threadAMethod(){
    ...
    while(commonVar == false){
         synchornized(lock){
              ...
              commonVar = true
         }
    }
}

void threadBMethod(){
    ...
    while(commonVar == true){
         synchornized(lock){
              ...
              commonVar = false
         }
    }
}

Así, cuando el hilo A entra en el whilebucle y mantiene el bloqueo, que hace lo que tiene que hacer y establecer el commonVara true. Luego, el hilo B entra, entra en el whilebucle y, como commonVares trueahora, puede mantener el bloqueo. Lo hace, ejecuta el synchronisedbloque y commonVarvuelve a establecer false. Ahora, el subproceso A vuelve a tener su nueva ventana de CPU, estaba a punto de abandonar el whilebucle, pero el subproceso B acaba de restablecerlo false, por lo que el ciclo se repite nuevamente. Los subprocesos hacen algo (por lo que no están bloqueados en el sentido tradicional) sino por casi nada.

Quizás también sea bueno mencionar que livelock no necesariamente tiene que aparecer aquí. Supongo que el planificador favorece al otro hilo una vez que el synchronisedbloque termina de ejecutarse. La mayoría de las veces, creo que es una expectativa difícil de alcanzar y depende de muchas cosas que suceden bajo el capó.

stdout
fuente
Buen ejemplo También ilustra por qué siempre debe leer y escribir atómicamente en un contexto concurrente. Si los bucles while estuvieran dentro de los bloques de sincronización, lo anterior no sería un problema.
CognizantApe