Scrivi un programma che andrà sicuramente in deadlock [chiuso]


86

Recentemente ho ricevuto queste domande in un'intervista.

Ho risposto che il deadlock si verifica se l'interleaving va male, ma l'intervistatore ha insistito sul fatto che un programma che andrà sempre in deadlock indipendentemente dall'interleaving può essere scritto.

Possiamo scrivere un programma del genere? Puoi indicarmi qualche programma di esempio come quello?


3
L'intervistatore è decisamente un tipo sciocco.
Lion

23
L'intervistatore non è certo un tipo sciocco. La piena comprensione di un argomento significa che dovresti essere in grado di spiegare i casi limite polari: fare un programma per non bloccare mai e per bloccare sempre.
Yuriy Zubarev

Risposte:


100

AGGIORNAMENTO: Questa domanda è stata oggetto del mio blog nel gennaio 2013 . Grazie per l'ottima domanda!


Come possiamo scrivere un programma che andrà sempre in deadlock indipendentemente da come sono pianificati i thread?

Ecco un esempio in C #. Notare che il programma sembra non contenere blocchi e dati condivisi. Ha solo una singola variabile locale e tre istruzioni, eppure si blocca con il 100% di certezza. Sarebbe difficile trovare un programma più semplice che si blocchi con certezza.

Esercizio per il lettore n. 1: spiega come si verifica questo deadlock. (Una risposta è nei commenti.)

Esercizio per il lettore n. 2: dimostra lo stesso deadlock in Java. (Una risposta è qui: 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
La mia conoscenza teorica del C # è limitata, ma presumo che il classloader garantisca che il codice venga eseguito a thread singolo come in Java. Sono abbastanza sicuro che ci sia un esempio simile in Java Puzzlers.
Voo

11
@Voo: hai una buona memoria. Neal Gafter - il coautore di "Java Puzzlers" - e io abbiamo presentato una versione un po 'più offuscata di questo codice nel nostro discorso "C # Puzzlers" alla Oslo Developer Conference di alcuni anni fa.
Eric Lippert

41
@Lieven: il costruttore statico deve essere eseguito non più di una volta e deve essere eseguito prima della prima chiamata a qualsiasi metodo statico nella classe. Main è un metodo statico, quindi il thread principale chiama il ctor statico. Per garantire che venga eseguito solo una volta, CLR rimuove un blocco che non viene rilasciato fino al termine del ctor statico. Quando il ctor avvia un nuovo thread, quel thread chiama anche un metodo statico, quindi CLR tenta di prendere il blocco per vedere se è necessario eseguire il ctor. Il thread principale nel frattempo "si unisce" al thread bloccato, e ora abbiamo il nostro deadlock.
Eric Lippert

33
@artbristol: non ho mai scritto solo una riga di codice Java; Non vedo motivo per iniziare adesso.
Eric Lippert

4
Oh, pensavo avessi una risposta al tuo esercizio n. 2. Congratulazioni per aver ricevuto così tanti voti positivi per aver risposto a una domanda su Java.
artbristol

27

Il latch qui assicura che entrambi i blocchi siano mantenuti quando ogni thread tenta di bloccare l'altro:

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

   }

}

È interessante eseguire jconsole, che mostrerà correttamente il deadlock nella scheda Thread.


3
Finora è il massimo, ma lo sostituirei sleepcon un fermo adeguato: in teoria, qui abbiamo una condizione di gara. Sebbene possiamo essere quasi sicuri che 0,5 sec siano sufficienti, non è troppo buono per un'attività di intervista.
alf

25

Si verifica un deadlock quando i thread (o qualunque cosa la tua piattaforma chiami le sue unità di esecuzione) acquisiscono risorse, dove ogni risorsa può essere trattenuta solo da un thread alla volta e mantiene tali risorse in modo tale che le conservazioni non possono essere anticipate e esiste una relazione "circolare" tra i thread in modo che ogni thread nel deadlock sia in attesa di acquisire una risorsa contenuta da un altro thread.

Quindi, un modo semplice per evitare il deadlock è dare un ordine totale alle risorse e imporre una regola secondo cui le risorse vengono acquisite solo dai thread in ordine . Al contrario, un deadlock può essere creato intenzionalmente eseguendo thread che acquisiscono risorse, ma non le acquisiscono in ordine. Per esempio:

Due fili, due serrature. Il primo thread esegue un ciclo che tenta di acquisire i blocchi in un certo ordine, il secondo thread esegue un ciclo che tenta di acquisire i blocchi nell'ordine opposto. Ogni thread rilascia entrambi i blocchi dopo aver acquisito correttamente i blocchi.

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

Ora, ci sono stati alcuni commenti in questa domanda che sottolineano la differenza tra la probabilità e la certezza dello stallo. In un certo senso, la distinzione è una questione accademica. Da un punto di vista pratico, mi piacerebbe sicuramente vedere un sistema in esecuzione che non si blocchi con il codice che ho scritto sopra :)

Tuttavia, le domande dell'intervista a volte possono essere accademiche, e questa domanda SO ha la parola "sicuramente" nel titolo, quindi quello che segue è un programma che certamente si blocca. LockerVengono creati due oggetti, a ciascuno vengono assegnati due blocchi e uno CountDownLatchutilizzato per la sincronizzazione tra i thread. Ciascuno Lockerblocca il primo blocco, quindi conta alla rovescia una volta. Quando entrambi i thread hanno acquisito un blocco e hanno contato alla rovescia il blocco, procedono oltre la barriera del blocco e tentano di acquisire un secondo blocco, ma in ogni caso l'altro thread contiene già il blocco desiderato. Questa situazione si traduce in un certo deadlock.

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
Ci scusiamo per aver citato Linus: "Parlare costa poco. Mostrami il codice." - è un bel compito, ed è sorprendentemente più difficile di quanto sembri.
alf

2
È possibile eseguire questo codice senza deadlock
Vladimir Zhilyaev

1
Ok, ragazzi siete brutali, ma penso che questa sia ora una risposta completa.
Greg Mattes

@GregMattes grazie :) Non posso aggiungere altro che +1, e spero vi siate divertiti :)
alf

15

Ecco un esempio Java seguendo quello di 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
Penso che l'utilizzo del metodo join in run sia poco fuorviante. Suggerisce che questo altro join oltre a quello nel blocco statico sia necessario per ottenere un deadlock mentre il deadlock è causato dall'istruzione "new Lock ()". La mia riscrittura, utilizzando il metodo statico come in C # esempio: stackoverflow.com/a/16203272/2098232
luke657

Puoi spiegare il tuo esempio?
gstackoverflow

secondo i miei esperimenti t.join (); Il metodo inside run () è ridondante
gstackoverflow

Ho rimosso il codice ridondante che impedisce la comprensione
gstackoverflow il

11

Ecco un esempio dalla documentazione:

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 Per collegare il tutorial Java.
MRE

4
"è estremamente probabile" non è abbastanza buono per "andrà sicuramente in stallo"
alf

1
@alf Oh ma la questione fondamentale è dimostrata abbastanza bene qui. Si può scrivere uno scheduler Round robin che espone un Object invokeAndWait(Callable task)metodo. Poi tutto Callable t1ha a che fare è invokeAndWait()per Callable t2durante il suo tempo di vita prima di tornare, e viceversa.
user268396

2
@ user268396 beh, la questione fondamentale è banale e noiosa :) Il punto centrale dell'attività è scoprire, o dimostrare di aver capito, che è sorprendentemente difficile ottenere un deadlock garantito (così come garantire qualsiasi cosa in un mondo asincrono ).
alf

4
@bezz sleepè noioso. Anche se credo che nessun thread verrà avviato per 5 secondi, è comunque una condizione di gara. Non vuoi assumere un programmatore su cui fare affidamento per sleep()risolvere le condizioni di gara :)
alf

9

Ho riscritto la versione Java di Yuriy Zubarev dell'esempio di deadlock pubblicato da Eric Lippert: https://stackoverflow.com/a/9286697/2098232 per assomigliare più da vicino alla versione C #. Se il blocco di inizializzazione di Java funziona in modo simile al costruttore statico C # e prima acquisisce il blocco, non è necessario un altro thread per invocare anche il metodo join per ottenere un deadlock, è sufficiente richiamare un metodo statico dalla classe Lock, come il C # originale esempio. La situazione di stallo risultante sembra confermarlo.

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

}

perché quando commento Lock.initialize () nel metodo run non si blocca? il metodo di inizializzazione non fa nulla però ??
Aequitas

@ Equitas è solo una supposizione, ma il metodo potrebbe essere ottimizzato; non sono sicuro di come funzionerebbe con i thread
Dave Cousineau

5

Non è un compito di intervista il più semplice che puoi ottenere: nel mio progetto, ha paralizzato il lavoro di una squadra per un'intera giornata. È molto facile fermare il programma, ma è molto difficile portarlo allo stato in cui il dump del thread scrive qualcosa del tipo,

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)

Quindi l'obiettivo sarebbe quello di ottenere un deadlock che JVM considererà un deadlock. Ovviamente, nessuna soluzione come

synchronized (this) {
    wait();
}

funzioneranno in quel senso, anche se si fermeranno per sempre. Affidarsi a una condizione di gara non è nemmeno una buona idea, poiché durante l'intervista di solito vuoi mostrare qualcosa che funziona in modo dimostrabile, non qualcosa che dovrebbe funzionare la maggior parte del tempo.

Ora, la sleep()soluzione va bene, in un certo senso è difficile immaginare una situazione in cui non funziona, ma non è giusto (siamo in uno sport equo, no?). La soluzione di @artbristol (la mia è la stessa, solo oggetti diversi come monitor) è carina, ma lunga e utilizza le nuove primitive di concorrenza per ottenere i thread nello stato giusto, il che non è molto divertente:

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

Ricordo che la synchronizedsola soluzione si adatta a 11..13 righe di codice (esclusi commenti e importazioni), ma devo ancora ricordare il trucco effettivo. Aggiornerò se lo faccio.

Aggiornamento: ecco una brutta soluzione su 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) {
            }
        }
    }
}

Nota che sostituiamo un latch con un monitor oggetto (usando "a"come oggetto).


Hum, penso che sia un compito equo per il colloquio. Ti chiede di capire veramente i deadlock e il blocco in Java. Non penso nemmeno che l'idea generale sia così difficile (assicurati che entrambi i thread possano continuare solo dopo che entrambi hanno bloccato la loro prima risorsa), dovresti solo ricordare CountdownLatch - ma come intervistatore aiuterei l'intervistato in quella parte se potesse spiegare esattamente di cosa aveva bisogno (non è una classe di cui la maggior parte degli sviluppatori ha bisogno e non puoi cercarla su Google in un'intervista). Mi piacerebbe ricevere domande così interessanti per le interviste!
Voo

@Voo nel momento in cui ci stavamo giocando, non c'erano latch in JDK, quindi era tutto a mano. E la differenza tra LOCKEDe waiting to lockè sottile, non qualcosa che leggi durante la colazione. Ma beh, probabilmente hai ragione. Fammi riformulare.
alf

4

Questa versione C #, immagino che java dovrebbe essere abbastanza simile.

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

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

2
Buono! Veramente il programma C # più breve per creare un deadlock se rimuovi le consoleistruzioni. Puoi semplicemente scrivere l'intera Mainfunzione come 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();
        }
    }
}

Il programma si blocca sempre perché ogni thread è in attesa alla barriera per gli altri thread, ma per attendere la barriera, il thread deve tenere il monitor acceso BadRunnable.class.


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

2

C'è un esempio in Java qui

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

Dove un rapitore entra in una situazione di stallo quando si rifiuta di rinunciare alla vittima fino a quando non ottiene i soldi ma il negoziatore si rifiuta di rinunciare al denaro finché non ottiene la vittima.


Questa implementazione non è pertinente come data. Alcuni pezzi di codice sembrano mancare. Tuttavia, l'idea generale che esprimi è corretta per quanto riguarda la contesa di risorse che porta a un deadlock.
Master Chief,

l'esempio è pedagogico quindi sono curioso del motivo per cui lo interpreti come non pertinente ... il codice mancante sono metodi vuoti in cui i nomi dei metodi dovrebbero essere utili (ma non mostrati per brevità)
Toby

1

Una semplice ricerca mi ha dato il seguente codice:

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

Fonte: Deadlock


3
"è estremamente probabile" non è abbastanza buono per "andrà sicuramente in stallo"
alf

1

Ecco un esempio in cui un thread che mantiene il blocco avvia un altro thread che vuole lo stesso blocco e quindi lo starter attende fino a quando l'inizio non finisce ... per sempre:

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

Ecco un esempio:

due thread sono in esecuzione, ognuno in attesa che l'altro rilasci il blocco

public class ThreadClass estende 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'esecuzione di questo porterebbe a un deadlock:

public class 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);


}

}

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.