Escriba un programa que seguramente entrará en un punto muerto [cerrado]

86

Recientemente me hicieron estas preguntas en una entrevista.

Respondí que se produce un punto muerto si el entrelazado sale mal, pero el entrevistador insistió en que se puede escribir un programa que siempre entrará en un punto muerto independientemente del entrelazado.

¿Podemos escribir un programa así? ¿Puede señalarme algún programa de ejemplo como ese?

usuario2434
fuente
3
El entrevistador es definitivamente un tipo tonto.
León
23
El entrevistador ciertamente no es un tipo tonto. La comprensión completa de un tema significa que debería poder explicar los casos de borde polar: hacer que el programa nunca se bloquee y siempre se bloquee.
Yuriy Zubarev

Respuestas:

100

ACTUALIZACIÓN: Esta pregunta fue el tema de mi blog en enero de 2013 . ¡Gracias por la gran pregunta!


¿Cómo podemos escribir un programa que siempre entrará en un punto muerto sin importar cómo estén programados los hilos?

Aquí hay un ejemplo en C #. Tenga en cuenta que el programa parece no contener bloqueos ni datos compartidos. Tiene una sola variable local y tres declaraciones y, sin embargo, se bloquea con un 100% de certeza. Sería difícil encontrar un programa más simple que se bloquee con certeza.

Ejercicio para el lector n. ° 1: explique cómo se bloquea esto. (La respuesta está en los comentarios).

Ejercicio para el lector 2: demuestre el mismo punto muerto en Java. (Una respuesta está aquí: https://stackoverflow.com/a/9286697/88656 )

class MyClass
{
  static MyClass() 
  {
    // Let's run the initialization on another thread!
    var thread = new System.Threading.Thread(Initialize);
    thread.Start();
    thread.Join();
  }

  static void Initialize() 
  { /* TODO: Add initialization code */ }

  static void Main() 
  { }
}
Eric Lippert
fuente
4
Mi conocimiento de C # teórico es limitado, pero supongo que el cargador de clases garantiza que el código se ejecuta en un solo subproceso como lo hace en Java. Estoy bastante seguro de que hay un ejemplo similar en Java Puzzlers.
Voo
11
@Voo: Tienes buena memoria. Neal Gafter, el coautor de "Java Puzzlers", y yo presentamos una versión bastante más confusa de este código en nuestra charla "C # Puzzlers" en la Conferencia de desarrolladores de Oslo hace unos años.
Eric Lippert
41
@Lieven: el constructor estático no debe ejecutarse más de una vez y debe ejecutarse antes de la primera llamada a cualquier método estático de la clase. Main es un método estático, por lo que el hilo principal llama al ctor estático. Para asegurarse de que solo se ejecute una vez, CLR saca un bloqueo que no se libera hasta que finaliza el ctor estático. Cuando el ctor inicia un nuevo hilo, ese hilo también llama a un método estático, por lo que CLR intenta tomar el bloqueo para ver si necesita ejecutar el ctor. Mientras tanto, el hilo principal "se une" al hilo bloqueado, y ahora tenemos nuestro punto muerto.
Eric Lippert
33
@artbristol: Nunca he escrito ni una línea de código Java; No veo ninguna razón para empezar ahora.
Eric Lippert
4
Oh, asumí que tenías una respuesta a tu Ejercicio # 2. Felicitaciones por obtener tantos votos positivos por responder una pregunta de Java.
artbristol
27

El pestillo aquí asegura que ambos bloqueos se mantengan cuando cada hilo intenta bloquear el otro:

import java.util.concurrent.CountDownLatch;

public class Locker extends Thread {

   private final CountDownLatch latch;
   private final Object         obj1;
   private final Object         obj2;

   Locker(Object obj1, Object obj2, CountDownLatch latch) {
      this.obj1 = obj1;
      this.obj2 = obj2;
      this.latch = latch;
   }

   @Override
   public void run() {
      synchronized (obj1) {

         latch.countDown();
         try {
            latch.await();
         } catch (InterruptedException e) {
            throw new RuntimeException();
         }
         synchronized (obj2) {
            System.out.println("Thread finished");
         }
      }

   }

   public static void main(String[] args) {
      final Object obj1 = new Object();
      final Object obj2 = new Object();
      final CountDownLatch latch = new CountDownLatch(2);

      new Locker(obj1, obj2, latch).start();
      new Locker(obj2, obj1, latch).start();

   }

}

Es interesante ejecutar jconsole, que le mostrará correctamente el punto muerto en la pestaña Subprocesos.

Artbristol
fuente
3
Eso es lo mejor hasta ahora, pero lo reemplazaría sleepcon un pestillo adecuado: en teoría, tenemos una condición de carrera aquí. Si bien podemos estar casi seguros de que 0,5 segundos es suficiente, no es demasiado bueno para una tarea de entrevista.
alf
25

El punto muerto ocurre cuando los subprocesos (o lo que sea que su plataforma llame a sus unidades de ejecución) adquieren recursos, donde cada recurso solo puede ser retenido por un subproceso a la vez, y retiene esos recursos de tal manera que las retenciones no pueden ser reemplazadas, y Existe una relación "circular" entre los subprocesos de modo que cada subproceso en el punto muerto está esperando adquirir algún recurso en poder de otro subproceso.

Por lo tanto, una manera fácil de evitar el punto muerto es dar un orden total a los recursos e imponer una regla de que los recursos solo se adquieren mediante subprocesos en orden . Por el contrario, un interbloqueo se puede crear intencionalmente ejecutando subprocesos que adquieren recursos, pero no los adquieren en orden. Por ejemplo:

Dos hilos, dos cerraduras. El primer hilo ejecuta un bucle que intenta adquirir los bloqueos en un orden determinado, el segundo hilo ejecuta un bucle que intenta adquirir los bloqueos en el orden opuesto. Cada hilo libera ambos bloqueos después de adquirirlos con éxito.

public class HighlyLikelyDeadlock {
    static class Locker implements Runnable {
        private Object first, second;

        Locker(Object first, Object second) {
            this.first = first;
            this.second = second;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (first) {
                    synchronized (second) {
                        System.out.println(Thread.currentThread().getName());
                    }
                }
            }
        }
    }

    public static void main(final String... args) {
        Object lock1 = new Object(), lock2 = new Object();
        new Thread(new Locker(lock1, lock2), "Thread 1").start();
        new Thread(new Locker(lock2, lock1), "Thread 2").start();
    }
}

Ahora, ha habido algunos comentarios en esta pregunta que señalan la diferencia entre la probabilidad y la certeza del punto muerto. En cierto sentido, la distinción es una cuestión académica. Desde un punto de vista práctico, ciertamente me gustaría ver un sistema en ejecución que no se bloquee con el código que he escrito anteriormente :)

Sin embargo, las preguntas de la entrevista pueden ser académicas a veces, y esta pregunta SO tiene la palabra "seguramente" en el título, por lo que lo que sigue es un programa que ciertamente se bloquea. Se Lockercrean dos objetos, a cada uno se le asignan dos bloqueos y uno se CountDownLatchutiliza para sincronizar entre los hilos. Cada uno Lockerbloquea el primer bloqueo y luego realiza una cuenta regresiva del pestillo una vez. Cuando ambos hilos han adquirido un bloqueo y han contado hacia atrás del pestillo, pasan la barrera del pestillo e intentan adquirir un segundo bloqueo, pero en cada caso el otro hilo ya mantiene el bloqueo deseado. Esta situación da como resultado un cierto punto muerto.

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

public class CertainDeadlock {
    static class Locker implements Runnable {
        private CountDownLatch latch;
        private Lock first, second;

        Locker(CountDownLatch latch, Lock first, Lock second) {
            this.latch = latch;
            this.first = first;
            this.second = second;
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            try {
                first.lock();
                latch.countDown();
                System.out.println(threadName + ": locked first lock");
                latch.await();
                System.out.println(threadName + ": attempting to lock second lock");
                second.lock();
                System.out.println(threadName + ": never reached");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(final String... args) {
        CountDownLatch latch = new CountDownLatch(2);
        Lock lock1 = new ReentrantLock(), lock2 = new ReentrantLock();
        new Thread(new Locker(latch, lock1, lock2), "Thread 1").start();
        new Thread(new Locker(latch, lock2, lock1), "Thread 2").start();
    }
}
Greg Mattes
fuente
3
Perdón por citar a Linus, "Hablar es barato. Muéstrame el código". Es una tarea agradable y sorprendentemente más difícil de lo que parece.
alf
2
Es posible ejecutar este código sin interbloqueo
Vladimir Zhilyaev
1
Ok, ustedes son brutales, pero creo que esta es una respuesta completa.
Greg Mattes
@GregMattes gracias :) No puedo agregar nada más que +1, y espero que te hayas divertido :)
alf
15

Aquí hay un ejemplo de Java siguiendo el de Eric Lippert:

public class Lock implements Runnable {

    static {
        System.out.println("Getting ready to greet the world");
        try {
            Thread t = new Thread(new Lock());
            t.start();
            t.join();
        } catch (InterruptedException ex) {
            System.out.println("won't see me");
        }
    }

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }

    public void run() {           
        Lock lock = new Lock();      
    }

}
Yuriy Zubarev
fuente
4
Creo que usar el método join in run es un poco engañoso. Sugiere que esta otra unión además de la del bloque estático es necesaria para obtener un interbloqueo, mientras que el interbloqueo es causado por la declaración "new Lock ()". Mi reescritura, usando el método estático como en el ejemplo de C #: stackoverflow.com/a/16203272/2098232
luke657
¿Podría explicar su ejemplo?
gstackoverflow
según mis experimentos t.join (); el método run () interno es redundante
gstackoverflow
Eliminé el
11

Aquí hay un ejemplo de la documentación:

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s"
                + "  has bowed to me!%n", 
                this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s"
                + " has bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}
NubladoMármol
fuente
2
+1 Para vincular el tutorial de Java.
ERM
4
"es muy probable" no es lo suficientemente bueno para "seguramente entrará en un punto muerto"
alf
1
@alf Oh, pero el problema fundamental se demuestra bastante bien aquí. Se puede escribir un programador Round robin que exponga un Object invokeAndWait(Callable task)método. Entonces todo lo Callable t1tiene que hacer es invokeAndWait()para Callable t2durante su tiempo de vida, antes de regresar, y viceversa.
user268396
2
@ user268396 bueno, el problema fundamental es trivial y aburrido :) El objetivo de la tarea es descubrir, o demostrar que lo entiendes, que es sorprendentemente difícil conseguir un punto muerto garantizado (así como garantizar cualquier cosa en un mundo asincrónico ).
alf
4
@bezz sleepes aburrido. Si bien creo que ningún hilo comenzará durante 5 segundos, de todos modos es una condición de carrera. Usted no quiere contratar a un programador que se basaría en sleep()resolver las condiciones de carrera :)
alf
9

He reescrito la versión Java de Yuriy Zubarev del ejemplo de interbloqueo publicado por Eric Lippert: https://stackoverflow.com/a/9286697/2098232 para que se parezca más a la versión C #. Si el bloque de inicialización de Java funciona de manera similar al constructor estático de C # y primero adquiere el bloqueo, no necesitamos otro hilo para invocar también el método de unión para obtener un punto muerto, solo necesita invocar algún método estático de la clase Lock, como el C # original ejemplo. El bloqueo resultante parece confirmar esto.

public class Lock {

    static {
        System.out.println("Getting ready to greet the world");
        try {
            Thread t = new Thread(new Runnable(){

                @Override
                public void run() {
                    Lock.initialize();
                }

            });
            t.start();
            t.join();
        } catch (InterruptedException ex) {
            System.out.println("won't see me");
        }
    }

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }

    public static void initialize(){
        System.out.println("Initializing");
    }

}
luke657
fuente
¿Por qué cuando comento Lock.initialize () en el método de ejecución no se bloquea? aunque el método de inicialización no hace nada ??
Aequitas
@Aequitas es solo una suposición, pero el método podría estar optimizándose; no estoy seguro de cómo funcionaría eso con los hilos
Dave Cousineau
5

No es una tarea de entrevista más simple que puedas conseguir: en mi proyecto, paralizó el trabajo de un equipo durante todo un día. Es muy fácil hacer que su programa se detenga, pero es muy difícil llevarlo al estado en el que el volcado de hilos escribe algo como,

Found one Java-level deadlock:
=============================
"Thread-2":
  waiting to lock monitor 7f91c5802b58 (object 7fb291380, a java.lang.String),
  which is held by "Thread-1"
"Thread-1":
  waiting to lock monitor 7f91c6075308 (object 7fb2914a0, a java.lang.String),
  which is held by "Thread-2"

Java stack information for the threads listed above:
===================================================
"Thread-2":
    at uk.ac.ebi.Deadlock.run(Deadlock.java:54)
    - waiting to lock <7fb291380> (a java.lang.String)
    - locked <7fb2914a0> (a java.lang.String)
    - locked <7f32a0760> (a uk.ac.ebi.Deadlock)
    at java.lang.Thread.run(Thread.java:680)
"Thread-1":
    at uk.ac.ebi.Deadlock.run(Deadlock.java:54)
    - waiting to lock <7fb2914a0> (a java.lang.String)
    - locked <7fb291380> (a java.lang.String)
    - locked <7f32a0580> (a uk.ac.ebi.Deadlock)
    at java.lang.Thread.run(Thread.java:680)

Entonces, el objetivo sería obtener un punto muerto que JVM considerará un punto muerto. Obviamente, ninguna solución como

synchronized (this) {
    wait();
}

funcionarán en ese sentido, aunque de hecho se detendrán para siempre. Confiar en una condición de carrera tampoco es una buena idea, ya que durante la entrevista generalmente desea mostrar algo que funciona de manera comprobable, no algo que debería funcionar la mayor parte del tiempo.

Ahora, la sleep()solución está bien en cierto sentido, es difícil imaginar una situación en la que no funcione, pero no sea justa (estamos en un deporte justo, ¿no?). La solución de @artbristol (la mía es la misma, solo diferentes objetos como monitores) es agradable, pero larga y usa las nuevas primitivas de concurrencia para poner los hilos en el estado correcto, lo cual no es tan divertido:

public class Deadlock implements Runnable {
    private final Object a;
    private final Object b;
    private final static CountDownLatch latch = new CountDownLatch(2);

    public Deadlock(Object a, Object b) {
        this.a = a;
        this.b = b;
    }

    public synchronized static void main(String[] args) throws InterruptedException {
        new Thread(new Deadlock("a", "b")).start();
        new Thread(new Deadlock("b", "a")).start();
    }

    @Override
    public void run() {
        synchronized (a) {
            latch.countDown();
            try {
                latch.await();
            } catch (InterruptedException ignored) {
            }
            synchronized (b) {
            }
        }
    }
}

Recuerdo que la synchronizedsolución -only se ajusta a 11..13 líneas de código (excluyendo comentarios e importaciones), pero aún tengo que recordar el truco real. Actualizaré si lo hago.

Actualización: aquí hay una solución desagradable en synchronized:

public class Deadlock implements Runnable {
    public synchronized static void main(String[] args) throws InterruptedException {
        synchronized ("a") {
            new Thread(new Deadlock()).start();
            "a".wait();
        }
        synchronized ("") {
        }
    }

    @Override
    public void run() {
        synchronized ("") {
            synchronized ("a") {
                "a".notifyAll();
            }
            synchronized (Deadlock.class) {
            }
        }
    }
}

Tenga en cuenta que reemplazamos un pestillo con un monitor de objeto (usando "a"como objeto).

alf
fuente
Hum, creo que es una tarea de entrevista justa. Le pide que comprenda realmente los puntos muertos y el bloqueo en Java. Tampoco creo que la idea general sea tan difícil (asegúrese de que ambos hilos solo puedan continuar después de que ambos hayan bloqueado su primer recurso), solo debe recordar CountdownLatch, pero como entrevistador ayudaría al entrevistado en esa parte si pudiera explicar qué es exactamente lo que necesitaba (no es una clase que la mayoría de los desarrolladores necesiten y no se puede buscar en Google en una entrevista). ¡Me encantaría recibir preguntas tan interesantes para las entrevistas!
Voo
@Voo en el momento en que estábamos jugando con él, no había pestillos en JDK, así que todo fue a mano. Y la diferencia entre LOCKEDy waiting to lockes sutil, no es algo que leas durante el desayuno. Pero bueno, probablemente tengas razón. Déjame reformular.
alf
4

Esta versión de C #, supongo que Java debería ser bastante similar.

static void Main(string[] args)
{
    var mainThread = Thread.CurrentThread;
    mainThread.Join();

    Console.WriteLine("Press Any key");
    Console.ReadKey();
}
gemasp
fuente
2
¡Buena! Verdaderamente el programa de C # más corto para crear un interbloqueo si elimina las consoledeclaraciones. Simplemente puede escribir toda la Mainfunción como Thread.CurrentThread.Join();.
RBT
3
import java.util.concurrent.CountDownLatch;

public class SO8880286 {
    public static class BadRunnable implements Runnable {
        private CountDownLatch latch;

        public BadRunnable(CountDownLatch latch) {
            this.latch = latch;
        }

        public void run() {
            System.out.println("Thread " + Thread.currentThread().getId() + " starting");
            synchronized (BadRunnable.class) {
                System.out.println("Thread " + Thread.currentThread().getId() + " acquired the monitor on BadRunnable.class");
                latch.countDown();
                while (true) {
                    try {
                        latch.await();
                    } catch (InterruptedException ex) {
                        continue;
                    }
                    break;
                }
            }
            System.out.println("Thread " + Thread.currentThread().getId() + " released the monitor on BadRunnable.class");
            System.out.println("Thread " + Thread.currentThread().getId() + " ending");
        }
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[2];
        CountDownLatch latch = new CountDownLatch(threads.length);
        for (int i = 0; i < threads.length; ++i) {
            threads[i] = new Thread(new BadRunnable(latch));
            threads[i].start();
        }
    }
}

El programa siempre se bloquea porque cada hilo está esperando en la barrera por los otros hilos, pero para esperar la barrera, el hilo debe estar sosteniendo el monitor BadRunnable.class.

Daniel Trebbien
fuente
3
} catch (InterruptedException ex) { continue; }... hermoso
artbristol
2

Hay un ejemplo en Java aquí

http://baddotrobot.com/blog/2009/12/24/deadlock/

Donde un secuestrador entra en un punto muerto cuando se niega a entregar a la víctima hasta que obtenga el efectivo, pero el negociador se niega a entregar el efectivo hasta que atrape a la víctima.

Toby
fuente
Esa implementación no es pertinente como se dio. Parece que faltan algunos fragmentos de código. Sin embargo, la idea general que expresa es correcta en lo que respecta a la contención de recursos que conduce a un punto muerto.
Master Chief
el ejemplo es pedagógico, así que tengo curiosidad por saber por qué lo interpreta como no pertinente ... el código que falta son métodos vacíos donde se supone que los nombres de los métodos son útiles (pero no se muestran por brevedad)
Toby
1

Una simple búsqueda me dio el siguiente código:

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s"
                + "  has bowed to me!%n", 
                this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s"
                + " has bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}

Fuente: Deadlock

bchetty
fuente
3
"es extremadamente probable" no es lo suficientemente bueno para "seguramente entrará en un punto muerto"
alf
1

Aquí hay una muestra en la que un hilo que mantiene el bloqueo inicia otro hilo que quiere el mismo bloqueo y luego el iniciador espera hasta que el inicio termine ... para siempre:

class OuterTask implements Runnable {
    private final Object lock;

    public OuterTask(Object lock) {
        this.lock = lock;
    }

    public void run() {
        System.out.println("Outer launched");
        System.out.println("Obtaining lock");
        synchronized (lock) {
            Thread inner = new Thread(new InnerTask(lock), "inner");
            inner.start();
            try {
                inner.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class InnerTask implements Runnable {
    private final Object lock;

    public InnerTask(Object lock) {
        this.lock = lock;
    }

    public void run() {
        System.out.println("Inner launched");
        System.out.println("Obtaining lock");
        synchronized (lock) {
            System.out.println("Obtained");
        }
    }
}

class Sample {
    public static void main(String[] args) throws InterruptedException {
        final Object outerLock = new Object();
        OuterTask outerTask = new OuterTask(outerLock);
        Thread outer = new Thread(outerTask, "outer");
        outer.start();
        outer.join();
    }
}
Victor Sorokin
fuente
0

Aquí hay un ejemplo:

dos subprocesos se están ejecutando, cada uno esperando que el otro libere el bloqueo

ThreadClass de clase pública extiende Thread {

String obj1,obj2;
ThreadClass(String obj1,String obj2){
    this.obj1=obj1;
    this.obj2=obj2;
    start();
}

public void run(){
    synchronized (obj1) {
        System.out.println("lock on "+obj1+" acquired");

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("waiting for "+obj2);
        synchronized (obj2) {
            System.out.println("lock on"+ obj2+" acquired");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }


}

}

Ejecutar esto conduciría a un punto muerto:

clase pública SureDeadlock {

public static void main(String[] args) {
    String obj1= new String("obj1");
    String obj2= new String("obj2");

    new ThreadClass(obj1,obj2);
    new ThreadClass(obj2,obj1);


}

}

Praveen Kumar
fuente