Buen ejemplo de livelock?

141

Entiendo qué es Livelock, pero me preguntaba si alguien tenía un buen ejemplo basado en código. Y por código, no me refiero a "dos personas que intentan cruzarse en un corredor". Si leo eso otra vez, perderé mi almuerzo.

Alex Miller
fuente
96
¿Qué tal una simulación de software de dos personas que intentan cruzarse en un corredor?
1800 INFORMACIÓN
36
¡Te maldigo! Perdí mi almuerzo!
Alex Miller
3
Extrañamente apropiado: seuss.wikia.com/wiki/The_Zax
NotMe
Broma relacionada para chicos curiosos: codingarchitect.wordpress.com/2006/01/18/…
Jorjon
44
Dos personas que intentan cruzarse en un corredor: gist.github.com/deepankarb/d2dd6f21bc49902376e614d3746b8965 : p
iceman

Respuestas:

119

Aquí hay un ejemplo muy simple de Java de livelock donde un esposo y una esposa están tratando de comer sopa, pero solo tienen una cuchara entre ellos. Cada cónyuge es demasiado educado y pasará la cuchara si el otro aún no ha comido.

public class Livelock {
    static class Spoon {
        private Diner owner;
        public Spoon(Diner d) { owner = d; }
        public Diner getOwner() { return owner; }
        public synchronized void setOwner(Diner d) { owner = d; }
        public synchronized void use() { 
            System.out.printf("%s has eaten!", owner.name); 
        }
    }

    static class Diner {
        private String name;
        private boolean isHungry;

        public Diner(String n) { name = n; isHungry = true; }       
        public String getName() { return name; }
        public boolean isHungry() { return isHungry; }

        public void eatWith(Spoon spoon, Diner spouse) {
            while (isHungry) {
                // Don't have the spoon, so wait patiently for spouse.
                if (spoon.owner != this) {
                    try { Thread.sleep(1); } 
                    catch(InterruptedException e) { continue; }
                    continue;
                }                       

                // If spouse is hungry, insist upon passing the spoon.
                if (spouse.isHungry()) {                    
                    System.out.printf(
                        "%s: You eat first my darling %s!%n", 
                        name, spouse.getName());
                    spoon.setOwner(spouse);
                    continue;
                }

                // Spouse wasn't hungry, so finally eat
                spoon.use();
                isHungry = false;               
                System.out.printf(
                    "%s: I am stuffed, my darling %s!%n", 
                    name, spouse.getName());                
                spoon.setOwner(spouse);
            }
        }
    }

    public static void main(String[] args) {
        final Diner husband = new Diner("Bob");
        final Diner wife = new Diner("Alice");

        final Spoon s = new Spoon(husband);

        new Thread(new Runnable() { 
            public void run() { husband.eatWith(s, wife); }   
        }).start();

        new Thread(new Runnable() { 
            public void run() { wife.eatWith(s, husband); } 
        }).start();
    }
}
Jeremy Elbourn
fuente
66
¿No getOwnertiene que sincronizarse también el método? Desde Java efectivo "la sincronización no tiene efecto a menos que tanto leer como escribir ".
Sanghyun Lee
¿No debería usarlo en Thread.join()lugar de hacerlo Thread.sleep(), porque quiere esperar a que el cónyuge coma?
Consuelo
¿Qué debemos hacer para superar el problema de Livelock en este ejemplo en particular?
Thor
1
El getOwnermétodo debe estar sincronizado ya que incluso si setOwnerestá sincronizado, esto no garantiza que el hilo que use getOwner(o que acceda al campo ownerdirectamente) vea los cambios realizados por el otro hilo setOwner. Este video explica esto con mucho cuidado: youtube.com/watch?v=WTVooKLLVT8
Timofey
2
No necesita usar synchronized palabras clave para el setOwnermétodo, porque leer y escribir es una acción atómica para la variable de referencia.
atiqkhaled
75

Dejando los comentarios al margen, un ejemplo que se sabe que aparece es en el código que intenta detectar y manejar situaciones de punto muerto. Si dos hilos detectan un punto muerto e intentan "hacerse a un lado" el uno para el otro, sin cuidado, terminarán atrapados en un bucle siempre "haciendo a un lado" y nunca logrando avanzar.

Por "hacer a un lado" quiero decir que liberarían la cerradura e intentarían que el otro la adquiriera. Podemos imaginar la situación con dos hilos haciendo esto (pseudocódigo):

// thread 1
getLocks12(lock1, lock2)
{
  lock1.lock();
  while (lock2.locked())
  {
    // attempt to step aside for the other thread
    lock1.unlock();
    wait();
    lock1.lock();
  }
  lock2.lock();
}

// thread 2
getLocks21(lock1, lock2)
{
  lock2.lock();
  while (lock1.locked())
  {
    // attempt to step aside for the other thread
    lock2.unlock();
    wait();
    lock2.lock();
  }
  lock1.lock();
}

Dejando a un lado las condiciones de carrera, lo que tenemos aquí es una situación en la que ambos hilos, si entran al mismo tiempo, terminarán corriendo en el bucle interno sin continuar. Obviamente este es un ejemplo simplificado. Una solución ingenua sería poner algún tipo de aleatoriedad en la cantidad de tiempo que esperarían los hilos.

La solución adecuada es respetar siempre la jerarquía de bloqueo . Elija un orden en el que adquiera los bloqueos y manténgalo. Por ejemplo, si ambos subprocesos siempre adquieren lock1 antes de lock2, entonces no hay posibilidad de punto muerto.

1800 INFORMACIÓN
fuente
Sí, entiendo eso. Estoy buscando un ejemplo de código real de tal. La pregunta es qué significa "hacerse a un lado" y cómo produce tal escenario.
Alex Miller
Entiendo que este es un ejemplo artificial, pero ¿es probable que esto pueda conducir a un bloqueo en vivo? ¿No sería mucho más probable que eventualmente se abriera una ventana donde una función pudiera capturar ambas, debido a inconsistencias en el tiempo en que los hilos están en voz alta para ejecutarse y cuando están programados?
DubiousPusher
Aunque no es un livelock estable porque obviamente saldrán de él eventualmente, creo que encaja bastante bien con la descripción
1800 INFORMACIÓN
Excelente y significativo ejemplo.
alecov
7

Como no hay una respuesta marcada como respuesta aceptada, he intentado crear un ejemplo de bloqueo en vivo;

El programa original fue escrito por mí en abril de 2012 para aprender varios conceptos de subprocesamiento múltiple. Esta vez lo he modificado para crear un punto muerto, condición de carrera, livelock, etc.

Entonces, primero comprendamos el enunciado del problema;

Problema de Cookie Maker

Hay algunos contenedores de ingredientes: ChocoPowederContainer , WheatPowderContainer . CookieMaker toma una cierta cantidad de polvo de los recipientes de ingredientes para hornear una cookie . Si un fabricante de galletas encuentra un contenedor vacío, busca otro contenedor para ahorrar tiempo. Y espera hasta que Filler llene el recipiente requerido. Hay un relleno que revisa el contenedor a intervalos regulares y llena cierta cantidad si un contenedor lo necesita.

Por favor revise el código completo en github ;

Déjame explicarte brevemente la implementación.

  • Comienzo Filler como hilo de demonio. Por lo tanto, seguirá llenando contenedores a intervalos regulares. Para llenar un contenedor primero, bloquea el contenedor -> verifica si necesita algo de polvo -> lo llena -> indica a todos los fabricantes que lo están esperando -> desbloquea el contenedor.
  • Creo CookieMaker y configuro que puede hornear hasta 8 cookies en paralelo. Y comienzo 8 hilos para hornear galletas.
  • Cada hilo del fabricante crea 2 sub-hilos invocables para tomar el polvo de los contenedores.
  • sub-thread toma un candado en un contenedor y verifica si tiene suficiente polvo. Si no, espera un poco de tiempo. Una vez que Filler llena el contenedor, toma el polvo y desbloquea el contenedor.
  • Ahora completa otras actividades como: hacer mezclas y hornear, etc.

Echemos un vistazo al código:

CookieMaker.java

private Integer getMaterial(final Ingredient ingredient) throws Exception{
        :
        container.lock();
        while (!container.getIngredient(quantity)) {
            container.empty.await(1000, TimeUnit.MILLISECONDS);
            //Thread.sleep(500); //For deadlock
        }
        container.unlock();
        :
}

IngredientContainer.java

public boolean getIngredient(int n) throws Exception {
    :
    lock();
    if (quantityHeld >= n) {
        TimeUnit.SECONDS.sleep(2);
        quantityHeld -= n;
        unlock();
        return true;
    }
    unlock();
    return false;
}

Todo funciona bien hasta que llene está llenando los contenedores. Pero si me olvido de iniciar el relleno, o si el relleno se va con una licencia inesperada, los subprocesos siguen cambiando sus estados para permitir que otro fabricante vaya y revise el contenedor.

También he creado un demonio ThreadTracer que vigila los estados de hilos y puntos muertos. Esta es la salida de la consola;

2016-09-12 21:31:45.065 :: [Maker_0:WAITING, Maker_1:WAITING, Maker_2:WAITING, Maker_3:WAITING, Maker_4:WAITING, Maker_5:WAITING, Maker_6:WAITING, Maker_7:WAITING, pool-7-thread-1:TIMED_WAITING, pool-7-thread-2:TIMED_WAITING, pool-8-thread-1:TIMED_WAITING, pool-8-thread-2:TIMED_WAITING, pool-6-thread-1:TIMED_WAITING, pool-6-thread-2:TIMED_WAITING, pool-5-thread-1:TIMED_WAITING, pool-5-thread-2:TIMED_WAITING, pool-1-thread-1:TIMED_WAITING, pool-3-thread-1:TIMED_WAITING, pool-2-thread-1:TIMED_WAITING, pool-1-thread-2:TIMED_WAITING, pool-4-thread-1:TIMED_WAITING, pool-4-thread-2:RUNNABLE, pool-3-thread-2:TIMED_WAITING, pool-2-thread-2:TIMED_WAITING]
2016-09-12 21:31:45.065 :: [Maker_0:WAITING, Maker_1:WAITING, Maker_2:WAITING, Maker_3:WAITING, Maker_4:WAITING, Maker_5:WAITING, Maker_6:WAITING, Maker_7:WAITING, pool-7-thread-1:TIMED_WAITING, pool-7-thread-2:TIMED_WAITING, pool-8-thread-1:TIMED_WAITING, pool-8-thread-2:TIMED_WAITING, pool-6-thread-1:TIMED_WAITING, pool-6-thread-2:TIMED_WAITING, pool-5-thread-1:TIMED_WAITING, pool-5-thread-2:TIMED_WAITING, pool-1-thread-1:TIMED_WAITING, pool-3-thread-1:TIMED_WAITING, pool-2-thread-1:TIMED_WAITING, pool-1-thread-2:TIMED_WAITING, pool-4-thread-1:TIMED_WAITING, pool-4-thread-2:TIMED_WAITING, pool-3-thread-2:TIMED_WAITING, pool-2-thread-2:TIMED_WAITING]
WheatPowder Container has 0 only.
2016-09-12 21:31:45.082 :: [Maker_0:WAITING, Maker_1:WAITING, Maker_2:WAITING, Maker_3:WAITING, Maker_4:WAITING, Maker_5:WAITING, Maker_6:WAITING, Maker_7:WAITING, pool-7-thread-1:TIMED_WAITING, pool-7-thread-2:TIMED_WAITING, pool-8-thread-1:TIMED_WAITING, pool-8-thread-2:TIMED_WAITING, pool-6-thread-1:TIMED_WAITING, pool-6-thread-2:TIMED_WAITING, pool-5-thread-1:TIMED_WAITING, pool-5-thread-2:TIMED_WAITING, pool-1-thread-1:TIMED_WAITING, pool-3-thread-1:TIMED_WAITING, pool-2-thread-1:TIMED_WAITING, pool-1-thread-2:TIMED_WAITING, pool-4-thread-1:TIMED_WAITING, pool-4-thread-2:TIMED_WAITING, pool-3-thread-2:TIMED_WAITING, pool-2-thread-2:RUNNABLE]
2016-09-12 21:31:45.082 :: [Maker_0:WAITING, Maker_1:WAITING, Maker_2:WAITING, Maker_3:WAITING, Maker_4:WAITING, Maker_5:WAITING, Maker_6:WAITING, Maker_7:WAITING, pool-7-thread-1:TIMED_WAITING, pool-7-thread-2:TIMED_WAITING, pool-8-thread-1:TIMED_WAITING, pool-8-thread-2:TIMED_WAITING, pool-6-thread-1:TIMED_WAITING, pool-6-thread-2:TIMED_WAITING, pool-5-thread-1:TIMED_WAITING, pool-5-thread-2:TIMED_WAITING, pool-1-thread-1:TIMED_WAITING, pool-3-thread-1:TIMED_WAITING, pool-2-thread-1:TIMED_WAITING, pool-1-thread-2:TIMED_WAITING, pool-4-thread-1:TIMED_WAITING, pool-4-thread-2:TIMED_WAITING, pool-3-thread-2:TIMED_WAITING, pool-2-thread-2:TIMED_WAITING]

Notarás que los subprocesos y cambian sus estados y esperan.

Amit Kumar Gupta
fuente
4

Un ejemplo real (aunque sin código exacto) es el bloqueo en vivo de dos procesos competidores en un intento de corregir un punto muerto del servidor SQL, y cada proceso utiliza el mismo algoritmo de espera-reintento para volver a intentarlo. Si bien es la suerte del momento, he visto que esto sucede en máquinas separadas con características de rendimiento similares en respuesta a un mensaje agregado a un tema de EMS (por ejemplo, guardar una actualización de un gráfico de un solo objeto más de una vez) y no poder controlar La orden de bloqueo.

Una buena solución en este caso sería tener consumidores competidores (evitar el procesamiento duplicado lo más arriba posible en la cadena al dividir el trabajo en objetos no relacionados).

Una solución menos deseable (ok, truco sucio) es romper la mala suerte del tiempo (tipo de diferencias de fuerza en el procesamiento) por adelantado o romperla después del punto muerto mediante el uso de diferentes algoritmos o algún elemento de aleatoriedad. Esto aún podría tener problemas porque es posible que la orden de bloqueo sea "pegajosa" para cada proceso, y esto lleva un cierto mínimo de tiempo que no se tiene en cuenta en el reintento de espera.

Otra solución más (al menos para SQL Server) es probar un nivel de aislamiento diferente (por ejemplo, una instantánea).

Equipo
fuente
2

Codifiqué el ejemplo de 2 personas que pasaban por un pasillo. Los dos hilos se evitarán en cuanto se den cuenta de que sus direcciones son las mismas.

public class LiveLock {
    public static void main(String[] args) throws InterruptedException {
        Object left = new Object();
        Object right = new Object();
        Pedestrian one = new Pedestrian(left, right, 0); //one's left is one's left
        Pedestrian two = new Pedestrian(right, left, 1); //one's left is two's right, so have to swap order
        one.setOther(two);
        two.setOther(one);
        one.start();
        two.start();
    }
}

class Pedestrian extends Thread {
    private Object l;
    private Object r;
    private Pedestrian other;
    private Object current;

    Pedestrian (Object left, Object right, int firstDirection) {
        l = left;
        r = right;
        if (firstDirection==0) {
            current = l;
        }
        else {
            current = r;
        }
    }

    void setOther(Pedestrian otherP) {
        other = otherP;
    }

    Object getDirection() {
        return current;
    }

    Object getOppositeDirection() {
        if (current.equals(l)) {
            return r;
        }
        else {
            return l;
        }
    }

    void switchDirection() throws InterruptedException {
        Thread.sleep(100);
        current = getOppositeDirection();
        System.out.println(Thread.currentThread().getName() + " is stepping aside.");
    }

    public void run() {
        while (getDirection().equals(other.getDirection())) {
            try {
                switchDirection();
                Thread.sleep(100);
            } catch (InterruptedException e) {}
        }
    }
} 
PoweredByRice
fuente
2

Versión C # del código de jelbourn:

using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace LiveLockExample
{
    static class Program
    {
        public static void Main(string[] args)
        {
            var husband = new Diner("Bob");
            var wife = new Diner("Alice");

            var s = new Spoon(husband);

            Task.WaitAll(
                Task.Run(() => husband.EatWith(s, wife)),
                Task.Run(() => wife.EatWith(s, husband))
                );
        }

        public class Spoon
        {
            public Spoon(Diner diner)
            {
                Owner = diner;
            }


            public Diner Owner { get; private set; }

            [MethodImpl(MethodImplOptions.Synchronized)]
            public void SetOwner(Diner d) { Owner = d; }

            [MethodImpl(MethodImplOptions.Synchronized)]
            public void Use()
            {
                Console.WriteLine("{0} has eaten!", Owner.Name);
            }
        }

        public class Diner
        {
            public Diner(string n)
            {
                Name = n;
                IsHungry = true;
            }

            public string Name { get; private set; }

            private bool IsHungry { get; set; }

            public void EatWith(Spoon spoon, Diner spouse)
            {
                while (IsHungry)
                {
                    // Don't have the spoon, so wait patiently for spouse.
                    if (spoon.Owner != this)
                    {
                        try
                        {
                            Thread.Sleep(1);
                        }
                        catch (ThreadInterruptedException e)
                        {
                        }

                        continue;
                    }

                    // If spouse is hungry, insist upon passing the spoon.
                    if (spouse.IsHungry)
                    {
                        Console.WriteLine("{0}: You eat first my darling {1}!", Name, spouse.Name);
                        spoon.SetOwner(spouse);
                        continue;
                    }

                    // Spouse wasn't hungry, so finally eat
                    spoon.Use();
                    IsHungry = false;
                    Console.WriteLine("{0}: I am stuffed, my darling {1}!", Name, spouse.Name);
                    spoon.SetOwner(spouse);
                }
            }
        }
    }
}
asostechnix
fuente
1

Un ejemplo aquí podría ser usar un tryLock cronometrado para obtener más de un bloqueo y, si no puede obtenerlos todos, retroceda e intente nuevamente.

boolean tryLockAll(Collection<Lock> locks) {
  boolean grabbedAllLocks = false;
  for(int i=0; i<locks.size(); i++) {
    Lock lock = locks.get(i);
    if(!lock.tryLock(5, TimeUnit.SECONDS)) {
      grabbedAllLocks = false;

      // undo the locks I already took in reverse order
      for(int j=i-1; j >= 0; j--) {
        lock.unlock();
      }
    }
  }
}

Me imagino que dicho código sería problemático ya que tiene muchos hilos colisionando y esperando obtener un conjunto de bloqueos. Pero no estoy seguro de que esto sea muy convincente para mí como un simple ejemplo.

Alex Miller
fuente
1
para que este sea un livelock necesitarás otro hilo para adquirir esos bloqueos en un orden diferente. Si todos los hilos se usan tryLockAll()con los bloqueos locksen el mismo orden, no hay livelock.
JaviMerino
0

Versión de Python del código de jelbourn:

import threading
import time
lock = threading.Lock()

class Spoon:
    def __init__(self, diner):
        self.owner = diner

    def setOwner(self, diner):
        with lock:
            self.owner = diner

    def use(self):
        with lock:
            "{0} has eaten".format(self.owner)

class Diner:
    def __init__(self, name):
        self.name = name
        self.hungry = True

    def eatsWith(self, spoon, spouse):
        while(self.hungry):
            if self != spoon.owner:
                time.sleep(1) # blocks thread, not process
                continue

            if spouse.hungry:
                print "{0}: you eat first, {1}".format(self.name, spouse.name)
                spoon.setOwner(spouse)
                continue

            # Spouse was not hungry, eat
            spoon.use()
            print "{0}: I'm stuffed, {1}".format(self.name, spouse.name)
            spoon.setOwner(spouse)

def main():
    husband = Diner("Bob")
    wife = Diner("Alice")
    spoon = Spoon(husband)

    t0 = threading.Thread(target=husband.eatsWith, args=(spoon, wife))
    t1 = threading.Thread(target=wife.eatsWith, args=(spoon, husband))
    t0.start()
    t1.start()
    t0.join()
    t1.join()

if __name__ == "__main__":
    main()
nflacco
fuente
Errores: en uso (), la impresión no se usa y, lo que es más importante, la bandera de hambre no está establecida en False.
RDA
0

Modifico la respuesta de @jelbourn. Cuando uno de ellos se da cuenta de que el otro tiene hambre, él (ella) debe soltar la cuchara y esperar otra notificación, para que ocurra un bloqueo en vivo.

public class LiveLock {
    static class Spoon {
        Diner owner;

        public String getOwnerName() {
            return owner.getName();
        }

        public void setOwner(Diner diner) {
            this.owner = diner;
        }

        public Spoon(Diner diner) {
            this.owner = diner;
        }

        public void use() {
            System.out.println(owner.getName() + " use this spoon and finish eat.");
        }
    }

    static class Diner {
        public Diner(boolean isHungry, String name) {
            this.isHungry = isHungry;
            this.name = name;
        }

        private boolean isHungry;
        private String name;


        public String getName() {
            return name;
        }

        public void eatWith(Diner spouse, Spoon sharedSpoon) {
            try {
                synchronized (sharedSpoon) {
                    while (isHungry) {
                        while (!sharedSpoon.getOwnerName().equals(name)) {
                            sharedSpoon.wait();
                            //System.out.println("sharedSpoon belongs to" + sharedSpoon.getOwnerName())
                        }
                        if (spouse.isHungry) {
                            System.out.println(spouse.getName() + "is hungry,I should give it to him(her).");
                            sharedSpoon.setOwner(spouse);
                            sharedSpoon.notifyAll();
                        } else {
                            sharedSpoon.use();
                            sharedSpoon.setOwner(spouse);
                            isHungry = false;
                        }
                        Thread.sleep(500);
                    }
                }
            } catch (InterruptedException e) {
                System.out.println(name + " is interrupted.");
            }
        }
    }

    public static void main(String[] args) {
        final Diner husband = new Diner(true, "husband");
        final Diner wife = new Diner(true, "wife");
        final Spoon sharedSpoon = new Spoon(wife);

        Thread h = new Thread() {
            @Override
            public void run() {
                husband.eatWith(wife, sharedSpoon);
            }
        };
        h.start();

        Thread w = new Thread() {
            @Override
            public void run() {
                wife.eatWith(husband, sharedSpoon);
            }
        };
        w.start();

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        h.interrupt();
        w.interrupt();

        try {
            h.join();
            w.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
Yi Zhang
fuente
0
package concurrently.deadlock;

import static java.lang.System.out;


/* This is an example of livelock */
public class Dinner {

    public static void main(String[] args) {
        Spoon spoon = new Spoon();
        Dish dish = new Dish();

        new Thread(new Husband(spoon, dish)).start();
        new Thread(new Wife(spoon, dish)).start();
    }
}


class Spoon {
    boolean isLocked;
}

class Dish {
    boolean isLocked;
}

class Husband implements Runnable {

    Spoon spoon;
    Dish dish;

    Husband(Spoon spoon, Dish dish) {
        this.spoon = spoon;
        this.dish = dish;
    }

    @Override
    public void run() {

        while (true) {
            synchronized (spoon) {
                spoon.isLocked = true;
                out.println("husband get spoon");
                try { Thread.sleep(2000); } catch (InterruptedException e) {}

                if (dish.isLocked == true) {
                    spoon.isLocked = false; // give away spoon
                    out.println("husband pass away spoon");
                    continue;
                }
                synchronized (dish) {
                    dish.isLocked = true;
                    out.println("Husband is eating!");

                }
                dish.isLocked = false;
            }
            spoon.isLocked = false;
        }
    }
}

class Wife implements Runnable {

    Spoon spoon;
    Dish dish;

    Wife(Spoon spoon, Dish dish) {
        this.spoon = spoon;
        this.dish = dish;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (dish) {
                dish.isLocked = true;
                out.println("wife get dish");
                try { Thread.sleep(2000); } catch (InterruptedException e) {}

                if (spoon.isLocked == true) {
                    dish.isLocked = false; // give away dish
                    out.println("wife pass away dish");
                    continue;
                }
                synchronized (spoon) {
                    spoon.isLocked = true;
                    out.println("Wife is eating!");

                }
                spoon.isLocked = false;
            }
            dish.isLocked = false;
        }
    }
}
Atanasio V.
fuente