Ecrire un programme qui ira sûrement dans une impasse [fermé]


86

J'ai récemment reçu cette question posée lors d'une interview.

J'ai répondu qu'une impasse se produit si l'entrelacement tourne mal, mais l'intervieweur a insisté sur le fait qu'un programme qui ira toujours dans l'impasse indépendamment de l'entrelacement peut être écrit.

Pouvons-nous écrire un tel programme? Pouvez-vous m'indiquer un exemple de programme comme celui-là?


3
L'intervieweur est définitivement un idiot.
Lion

23
L'enquêteur n'est certainement pas un idiot. Une compréhension complète d'un sujet signifie que vous devriez être en mesure d'expliquer les cas de bord polaire: faire en sorte que le programme ne se verrouille jamais et qu'il se verrouille toujours.
Yuriy Zubarev

Réponses:


100

MISE À JOUR: Cette question a fait l'objet de mon blog en janvier 2013 . Merci pour la bonne question!


Comment pouvons-nous écrire un programme qui ira toujours dans une impasse quelle que soit la planification des threads?

Voici un exemple en C #. Notez que le programme semble ne contenir aucun verrou et aucune donnée partagée. Il n'a qu'une seule variable locale et trois déclarations, et pourtant il se bloque avec une certitude à 100%. On aurait du mal à proposer un programme plus simple qui bloque avec certitude.

Exercice au lecteur n ° 1: expliquez comment cette impasse. (Une réponse est dans les commentaires.)

Exercice au lecteur n ° 2: démontrez le même blocage en Java. (Une réponse est ici: 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() 
  { }
}

4
Ma connaissance du C # théorique est limitée, mais je suppose que le chargeur de classe garantit que le code est exécuté avec un seul thread comme il le fait en Java. Je suis presque sûr qu'il existe un exemple similaire dans Java Puzzlers.
Voo le

11
@Voo: Vous avez une bonne mémoire. Neal Gafter - le co-auteur de "Java Puzzlers" - et moi avons présenté une version un peu plus obscure de ce code dans notre conférence "C # Puzzlers" à la conférence des développeurs d'Oslo il y a quelques années.
Eric Lippert

41
@Lieven: Le constructeur statique ne doit pas s'exécuter plus d' une fois et il doit s'exécuter avant le premier appel à une méthode statique de la classe. Main est une méthode statique, donc le thread principal appelle le ctor statique. Pour s'assurer qu'il ne s'exécute qu'une seule fois, le CLR supprime un verrou qui n'est pas libéré tant que le moteur statique n'est pas terminé. Lorsque le ctor démarre un nouveau thread, ce thread appelle également une méthode statique, de sorte que le CLR essaie de prendre le verrou pour voir s'il a besoin d'exécuter le ctor. Le thread principal entre-temps "rejoint" le thread bloqué, et maintenant nous avons notre impasse.
Eric Lippert

33
@artbristol: Je n'ai jamais autant écrit qu'une ligne de code Java; Je ne vois aucune raison de commencer maintenant.
Eric Lippert

4
Oh, j'ai supposé que vous aviez une réponse à votre exercice n ° 2. Félicitations pour avoir obtenu autant de votes positifs pour avoir répondu à une question Java.
artbristol

27

Le verrou ici garantit que les deux verrous sont maintenus lorsque chaque thread tente de verrouiller l'autre:

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();

   }

}

Intéressant d'exécuter jconsole, qui vous montrera correctement le blocage dans l'onglet Threads.


3
C'est le meilleur jusqu'à présent, mais je remplacerais sleeppar un verrou approprié: théoriquement, nous avons une condition de course ici. Bien que nous puissions être presque sûrs que 0,5 seconde suffit, ce n'est pas trop bon pour une tâche d'entrevue.
alf

25

Le blocage se produit lorsque les threads (ou tout ce que votre plate-forme appelle ses unités d'exécution) acquièrent des ressources, où chaque ressource ne peut être détenue que par un thread à la fois, et conserve ces ressources de telle sorte que les réservations ne peuvent pas être anticipées, et il existe une relation "circulaire" entre les threads de telle sorte que chaque thread dans le blocage attend d'acquérir une ressource détenue par un autre thread.

Ainsi, un moyen simple d'éviter les blocages est de donner un ordre total aux ressources et d'imposer une règle selon laquelle les ressources ne sont acquises que par les threads dans l'ordre . Inversement, un blocage peut être intentionnellement créé en exécutant des threads qui acquièrent des ressources, mais ne les acquièrent pas dans l'ordre. Par exemple:

Deux fils, deux verrous. Le premier thread exécute une boucle qui tente d'acquérir les verrous dans un certain ordre, le second thread exécute une boucle qui tente d'acquérir les verrous dans l'ordre opposé. Chaque thread libère les deux verrous après avoir réussi l'acquisition des verrous.

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();
    }
}

Maintenant, il y a eu quelques commentaires dans cette question qui soulignent la différence entre la vraisemblance et la certitude d'une impasse. Dans un certain sens, la distinction est une question académique. D'un point de vue pratique, j'aimerais certainement voir un système en cours d'exécution qui ne se bloque pas avec le code que j'ai écrit ci-dessus :)

Cependant, les questions d'entrevue peuvent parfois être théoriques, et cette question SO a le mot «sûrement» dans le titre, donc ce qui suit est un programme qui bloque certainement . Deux Lockerobjets sont créés, chacun reçoit deux verrous et un CountDownLatchutilisé pour se synchroniser entre les threads. Chacun Lockerverrouille le premier verrou, puis compte à rebours le verrou une fois. Lorsque les deux fils ont acquis un verrou et ont décompté le verrou, ils passent devant la barrière de verrou et tentent d'acquérir un second verrou, mais dans chaque cas, l'autre fil détient déjà le verrou souhaité. Cette situation entraîne une certaine impasse.

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();
    }
}

3
Désolé d'avoir cité Linus, "Parler n'est pas cher. Montrez-moi le code." - c'est une tâche agréable, et c'est étonnamment plus difficile qu'il n'y paraît.
alf

2
Il est possible d'exécuter ce code sans blocage
Vladimir Zhilyaev

1
Ok, vous êtes brutaux, mais je pense que c'est maintenant une réponse complète.
Greg Mattes

@GregMattes merci :) Je ne peux rien ajouter sauf +1, et j'espère que vous vous êtes bien amusé :)
alf

15

Voici un exemple Java en suivant celui d'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();      
    }

}

4
Je pense que l'utilisation de la méthode join in run est peu trompeuse. Cela suggère que cette autre jointure en plus de celle du bloc statique est nécessaire pour obtenir un blocage alors que le blocage est causé par l'instruction "new Lock ()". Ma réécriture, en utilisant la méthode statique comme dans l'exemple C #: stackoverflow.com/a/16203272/2098232
luke657

Pouvez-vous expliquer votre exemple?
gstackoverflow

d'après mes expériences t.join (); à l'intérieur de la méthode run () est redondante
gstackoverflow

J'ai supprimé le code redondant qui empêche de comprendre
gstackoverflow

11

Voici un exemple de la documentation:

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();
    }
}

2
+1 Pour lier le didacticiel java.
mre

4
"il est extrêmement probable" n'est pas assez bon pour "va sûrement entrer dans une impasse"
alf

1
@alf Oh mais la question fondamentale est assez bien démontrée ici. On peut écrire un planificateur Round Robin qui expose une Object invokeAndWait(Callable task)méthode. Ensuite , tout Callable t1a à faire est invokeAndWait()pour Callable t2pendant sa durée de vie avant de revenir, et vice versa.
user268396

2
@ user268396 eh bien, le problème fondamental est trivial et ennuyeux :) Le but de la tâche est de découvrir - ou de prouver que vous comprenez - qu'il est étonnamment difficile d'obtenir une impasse garantie (ainsi que de garantir quoi que ce soit dans un monde asynchrone ).
alf

4
@bezz sleepest ennuyeux. Bien que je pense qu'aucun thread ne démarrera pendant 5 secondes, c'est de toute façon une condition de concurrence. Vous ne voulez pas embaucher un programmeur sur lequel s'appuyer pour sleep()résoudre les conditions de course :)
alf

9

J'ai réécrit la version Java de Yuriy Zubarev de l'exemple de blocage posté par Eric Lippert: https://stackoverflow.com/a/9286697/2098232 pour ressembler plus étroitement à la version C #. Si le bloc d'initialisation de Java fonctionne de la même manière que le constructeur statique C # et acquiert d'abord le verrou, nous n'avons pas besoin d'un autre thread pour invoquer également la méthode de jointure pour obtenir un blocage, il suffit d'appeler une méthode statique de la classe Lock, comme le C # d'origine exemple. L'impasse qui en résulte semble le confirmer.

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");
    }

}

pourquoi quand je commente Lock.initialize () dans la méthode d'exécution, il ne se bloque pas? la méthode initialize ne fait rien cependant ??
Aequitas

@Aequitas juste une supposition mais la méthode pourrait être optimisée; je ne sais pas comment cela fonctionnerait avec les fils
Dave Cousineau

5

Ce n'est pas une tâche d'entrevue la plus simple que vous puissiez obtenir: dans mon projet, cela a paralysé le travail d'une équipe pendant une journée entière. Il est très facile d'arrêter votre programme, mais il est très difficile de l'amener à l'état où thread dump écrit quelque chose comme,

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)

Le but serait donc d'obtenir un blocage que JVM considérera comme un blocage. Evidemment, aucune solution comme

synchronized (this) {
    wait();
}

fonctionnera dans ce sens, même s’ils s’arrêteront pour toujours. Se fier à une condition de race n'est pas non plus une bonne idée, car pendant l'entretien, vous voulez généralement montrer quelque chose qui fonctionne de manière prouvée, pas quelque chose qui devrait fonctionner la plupart du temps.

Maintenant, la sleep()solution est correcte dans un sens, il est difficile d'imaginer une situation où cela ne fonctionne pas, mais pas juste (nous sommes dans un sport équitable, n'est-ce pas?). La solution de @artbristol (la mienne est la même, juste des objets différents comme moniteurs) est agréable, mais longue et utilise les nouvelles primitives de concurrence pour obtenir les threads dans le bon état, ce qui n'est pas très amusant:

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) {
            }
        }
    }
}

Je me souviens que la synchronizedsolution -only correspond à 11..13 lignes de code (à l'exclusion des commentaires et des importations), mais je n'ai pas encore rappelé l'astuce réelle. Mettra à jour si je le fais.

Mise à jour: voici une solution moche sur 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) {
            }
        }
    }
}

Notez que nous remplaçons un verrou par un moniteur d'objet (en utilisant "a"comme objet).


Hum je pense que c'est une tâche d'entrevue juste. Il vous demande de vraiment comprendre les blocages et le verrouillage en Java. Je ne pense pas que l'idée générale soit aussi difficile (assurez-vous que les deux fils ne peuvent continuer qu'une fois que les deux ont verrouillé leur première ressource), vous devez simplement vous souvenir de CountdownLatch - mais en tant qu'intervieweur, j'aiderais l'interviewé à ce stade. s'il pouvait expliquer exactement ce dont il avait besoin (ce n'est pas un cours dont la plupart des développeurs ont jamais besoin et vous ne pouvez pas le rechercher sur Google dans une interview). J'adorerais avoir des questions aussi intéressantes pour les interviews!
Voo le

@Voo au moment où nous jouions avec, il n'y avait pas de verrous dans JDK, donc tout était à la main. Et la différence entre LOCKEDet waiting to lockest subtile, ce n'est pas quelque chose que vous lisez pendant le petit-déjeuner. Mais bon, vous avez probablement raison. Permettez-moi de reformuler.
alf

4

Cette version C #, je suppose que java devrait être assez similaire.

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

    Console.WriteLine("Press Any key");
    Console.ReadKey();
}

2
Bon! Vraiment le programme C # le plus court pour créer un blocage si vous supprimez les consoleinstructions. Vous pouvez simplement écrire la Mainfonction entière sous la forme 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();
        }
    }
}

Le programme se bloque toujours parce que chaque thread attend à la barrière pour les autres threads, mais pour attendre la barrière, le thread doit maintenir le moniteur sous tension BadRunnable.class.


3
} catch (InterruptedException ex) { continue; }... magnifique
artbristol

2

Il y a un exemple en Java ici

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

Où un kidnappeur se retrouve dans une impasse quand il refuse d'abandonner la victime jusqu'à ce qu'il reçoive l'argent mais le négociateur refuse de donner l'argent jusqu'à ce qu'il ait la victime.


Cette mise en œuvre n'est pas pertinente telle qu'elle est donnée. Certains morceaux de code semblent manquer. Cependant, l'idée générale que vous exprimez est correcte en ce qui concerne les conflits de ressources menant à une impasse.
Master Chief

l'exemple est pédagogique, donc je suis curieux de savoir pourquoi vous l'interprétez comme non pertinent ... le code manquant sont des méthodes vides où les noms de méthode sont censés être utiles (mais non montrés par souci de concision)
Toby

1

Une simple recherche m'a donné le code suivant:

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();
    }
}

Source: impasse


3
"il est extrêmement probable" n'est pas assez bon pour "va sûrement entrer dans une impasse"
alf

1

Voici un exemple où un thread tenant un verrou démarre un autre thread qui veut le même verrou, puis le démarreur attend que le démarrage se termine ... pour toujours:

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();
    }
}

0

Voici un exemple:

deux threads sont en cours d'exécution, chacun attendant que l'autre libère le verrou

La classe publique ThreadClass étend 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();
            }
        }

    }


}

}

L'exécution de ceci conduirait à une impasse:

classe publique 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);


}

}

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.