Escreva um programa que certamente entrará em um impasse [fechado]

86

Recentemente, recebi essas perguntas em uma entrevista.

Respondi que o impasse ocorre se a intercalação der errado, mas o entrevistador insistiu que um programa que sempre entrará em impasse, independentemente da intercalação, pode ser escrito.

Podemos escrever esse programa? Você pode me indicar algum programa de exemplo como esse?

user2434
fonte
3
O entrevistador é definitivamente um sujeito tolo.
Leão
23
O entrevistador certamente não é um sujeito tolo. Compreensão total de um assunto significa que você deve ser capaz de explicar casos extremos polares: fazendo o programa nunca travar e sempre travar.
Yuriy Zubarev de

Respostas:

100

ATUALIZAÇÃO: Esta questão foi o assunto do meu blog em janeiro de 2013 . Obrigado pela ótima pergunta!


Como podemos escrever um programa que sempre entrará em impasse, não importa como os threads estão agendados?

Aqui está um exemplo em C #. Observe que o programa parece não conter bloqueios e dados compartilhados. Ele tem apenas uma única variável local e três instruções e, ainda assim, bloqueia com 100% de certeza. Seria difícil chegar a um programa mais simples que travasse com certeza.

Exercício para o leitor # 1: explique como isso bloqueia. (Uma resposta está nos comentários.)

Exercício para o leitor 2: demonstrar o mesmo impasse em Java. (Uma resposta está aqui: 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
fonte
4
Meu conhecimento de C # teórico é limitado, mas presumo que o carregador de classe garanta que o código seja executado em uma única thread, como acontece em Java. Tenho certeza de que há um exemplo semelhante em Java Puzzlers.
Voo
11
@Voo: Você tem boa memória. Neal Gafter - o coautor de "Java Puzzlers" - e eu apresentei uma versão um pouco mais ofuscada desse código em nossa palestra "C # Puzzlers" na Conferência de Desenvolvedores de Oslo alguns anos atrás.
Eric Lippert
41
@Lieven: O construtor estático deve ser executado no máximo uma vez e deve ser executado antes da primeira chamada para qualquer método estático na classe. Main é um método estático, então o thread principal chama o ctor estático. Para garantir que ele seja executado apenas uma vez, o CLR tira um bloqueio que não é liberado até que o ctor estático termine. Quando o ctor inicia um novo encadeamento, esse encadeamento também chama um método estático, então o CLR tenta pegar o bloqueio para ver se ele precisa executar o ctor. Enquanto isso, o encadeamento principal "se junta" ao encadeamento bloqueado e agora temos nosso impasse.
Eric Lippert
33
@artbristol: Nunca escrevi nem mesmo uma linha de código Java; Não vejo razão para começar agora.
Eric Lippert
4
Oh, presumi que você tivesse uma resposta para o seu Exercício # 2. Parabéns por obter tantos votos positivos para responder a uma pergunta sobre Java.
artbristol
27

A trava aqui garante que ambos os bloqueios sejam mantidos quando cada thread tenta bloquear o outro:

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 executar o jconsole, que mostrará corretamente o impasse na guia Threads.

Artbristol
fonte
3
É o melhor até agora, mas eu substituiria sleeppor uma trava adequada: teoricamente, temos uma condição de corrida aqui. Embora possamos ter quase certeza de que 0,5 segundo é o suficiente, não é muito bom para uma tarefa de entrevista.
alf
25

O deadlock acontece quando os threads (ou o que quer que sua plataforma chame de unidades de execução) adquirem recursos, onde cada recurso só pode ser mantido por um thread de cada vez e retém esses recursos de tal forma que as suspensões não podem ser impedidas, e existe algum relacionamento "circular" entre os threads de modo que cada thread no conflito está esperando para adquirir algum recurso mantido por outro thread.

Portanto, uma maneira fácil de evitar o conflito é dar uma ordem total aos recursos e impor uma regra de que os recursos só são adquiridos por threads em ordem . Por outro lado, um deadlock pode ser criado intencionalmente executando threads que adquirem recursos, mas não os adquirem em ordem. Por exemplo:

Dois fios, duas fechaduras. O primeiro thread executa um loop que tenta adquirir os bloqueios em uma determinada ordem, o segundo thread executa um loop que tenta adquirir os bloqueios na ordem oposta. Cada thread libera ambos os bloqueios após adquiri-los com sucesso.

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

Agora, houve alguns comentários nesta questão que apontam a diferença entre o probabilidade e a certeza de um impasse. Em certo sentido, a distinção é uma questão acadêmica. Do ponto de vista prático, certamente gostaria de ver um sistema em execução que não bloqueie com o código que escrevi acima :)

No entanto, as perguntas da entrevista podem ser acadêmicas às vezes, e essa pergunta do SO tem a palavra "certamente" no título, então o que se segue é um programa que certamente bloqueia. Dois Lockerobjetos são criados, cada um recebe dois bloqueios e um CountDownLatchusado para sincronizar entre os threads. Cada Lockertrava a primeira fechadura e, em seguida, conta uma vez. Quando os dois fios adquirem um bloqueio e fazem a contagem regressiva do trinco, passam pela barreira do trinco e tentam obter um segundo bloqueio, mas em cada caso o outro segmento já contém o bloqueio desejado. Essa situação resulta em um certo 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();
    }
}
Greg Mattes
fonte
3
Desculpe por citar Linus, "Falar é barato. Mostre-me o código." - é uma boa tarefa e é surpreendentemente mais difícil do que parece.
alf
2
É possível executar este código sem deadlock
Vladimir Zhilyaev
1
Ok, vocês são brutais, mas acho que agora é uma resposta completa.
Greg Mattes
@GregMattes obrigado :) Não posso adicionar nada além de +1, e espero que você tenha se divertido :)
alf
15

Aqui está um exemplo de Java seguindo o 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
fonte
4
Acho que usar o método join in run é um pouco enganador. Ele sugere que essa outra junção além daquela no bloco estático é necessária para obter um deadlock enquanto o deadlock é causado pela instrução "new Lock ()". Minha reescrita, usando o método estático como no exemplo C #: stackoverflow.com/a/16203272/2098232
luke657
Você poderia explicar seu exemplo?
gstackoverflow de
de acordo com minhas experiências t.join (); O método run () interno é redundante
gstackoverflow de
Removi o código redundante que impede a compreensão
gstackoverflow
11

Aqui está um exemplo da documentação:

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();
    }
}
CloudyMarble
fonte
2
1 Para vincular o tutorial java.
mre
4
"é extremamente provável" não é bom o suficiente para "certamente entrará em um impasse"
alf
1
@alf Oh, mas a questão fundamental é demonstrada muito bem aqui. Pode-se escrever um agendador round robin que expõe um Object invokeAndWait(Callable task)método. Então tudo que Callable t1tem a fazer é invokeAndWait()para Callable t2durante o seu tempo de vida antes de retornar, e vice-versa.
user268396
2
@ user268396 bem, a questão fundamental é trivial e entediante :) O objetivo da tarefa é descobrir - ou provar que você entende - que é surpreendentemente difícil obter um deadlock garantido (bem como garantir qualquer coisa em um mundo assíncrono )
alf
4
@bezz sleepé chato. Embora eu acredite que nenhum tópico será iniciado por 5 segundos, é uma condição de corrida, de qualquer maneira. Você não quer contratar um programador que confiaria sleep()na resolução de condições de corrida :)
alf
9

Reescrevi a versão Java de Yuriy Zubarev do exemplo de deadlock postado por Eric Lippert: https://stackoverflow.com/a/9286697/2098232 para se parecer mais com a versão C #. Se o bloco de inicialização do Java funciona de forma semelhante ao construtor estático C # e primeiro adquire o bloqueio, não precisamos de outro thread para também invocar o método de junção para obter um deadlock, ele só precisa invocar algum método estático da classe Lock, como o C # original exemplo. O impasse resultante parece confirmar isso.

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
fonte
por que quando eu comento Lock.initialize () no método run ele não bloqueia? o método de inicialização não faz nada ??
Aequitas
@Aequitas é apenas um palpite, mas o método pode estar sendo otimizado; não tenho certeza sobre como isso funcionaria com threads
Dave Cousineau
5

Não é a tarefa de entrevista mais simples que você pode conseguir: no meu projeto, paralisou o trabalho de uma equipe por um dia inteiro. É muito fácil fazer seu programa parar, mas é muito difícil levá-lo ao estado em que o despejo de thread grava 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)

Portanto, o objetivo seria obter um conflito que a JVM considerará um conflito. Obviamente, nenhuma solução como

synchronized (this) {
    wait();
}

funcionarão nesse sentido, embora realmente parem para sempre. Depender de uma condição de corrida também não é uma boa ideia, já que durante a entrevista você geralmente quer mostrar algo que provavelmente está funcionando, não algo que deveria funcionar na maior parte do tempo.

Agora, a sleep()solução está bem em um sentido, é difícil imaginar uma situação em que não funcione, mas não é justo (estamos em um esporte justo, não é?). A solução de @artbristol (a minha é a mesma, apenas objetos diferentes como monitores) é boa, mas longa e usa as novas primitivas de simultaneidade para colocar os threads no estado certo, o que não é muito 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) {
            }
        }
    }
}

Eu me lembro que o synchronized solução -only se encaixa 11..13 linhas de código (excluindo comentários e importações), mas ainda não me lembrei do truque real. Atualizará se eu fizer.

Atualização: aqui está uma solução feia em 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) {
            }
        }
    }
}

Observe que substituímos uma trava por um monitor de objeto (usando "a"como um objeto).

alf
fonte
Hum, acho que é uma tarefa de entrevista justa. Ele pede que você realmente entenda os bloqueios e bloqueios em Java. Eu não acho que a ideia geral seja tão difícil (certifique-se de que ambos os tópicos só possam continuar depois de ambos terem bloqueado seu primeiro recurso), você apenas deve se lembrar do CountdownLatch - mas como entrevistador, eu ajudaria o entrevistado nessa parte se ele pudesse explicar o que exatamente ele precisava (não é uma classe que a maioria dos desenvolvedores precisa e você não pode pesquisar no Google em uma entrevista). Eu adoraria receber perguntas tão interessantes para entrevistas!
Voo
@Voo na época em que estávamos brincando com ele, não havia travas no JDK, então era tudo à mão. E a diferença entre LOCKEDe waiting to locké sutil, não algo que você lê durante o café da manhã. Mas bem, você provavelmente está certo. Deixe-me reformular.
alf
4

Esta versão C #, acho que o java deve ser bastante semelhante.

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

    Console.WriteLine("Press Any key");
    Console.ReadKey();
}
gemasp
fonte
2
Um bom! Realmente o programa C # mais curto para criar um deadlock se você remover as consoleinstruções. Você pode simplesmente escrever a Mainfunção inteira como Thread.CurrentThread.Join();.
RBT de
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();
        }
    }
}

O programa sempre bloqueia porque cada thread está esperando na barreira pelos outros threads, mas para esperar a barreira, a thread deve estar segurando o monitor BadRunnable.class.

Daniel Trebbien
fonte
3
} catch (InterruptedException ex) { continue; }... linda
artbristol
2

Há um exemplo em Java aqui

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

Onde um sequestrador entra em um impasse quando se recusa a entregar a vítima até conseguir o dinheiro, mas o negociador se recusa a entregar o dinheiro até pegar a vítima.

Toby
fonte
Essa implementação não é pertinente conforme fornecido. Algumas partes do código parecem estar faltando. No entanto, a ideia geral expressa está correta no que diz respeito à contenção de recursos que leva ao conflito.
Master Chief
o exemplo é pedagógico, então estou curioso para saber por que você o interpreta como não pertinente ... os códigos ausentes são métodos vazios onde os nomes dos métodos devem ser úteis (mas não mostrados por brevidade)
Toby
1

Uma simples pesquisa me deu o seguinte 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();
    }
}

Fonte: Deadlock

bchetty
fonte
3
"é extremamente provável" não é bom o suficiente para "certamente entrará em um impasse"
alf
1

Aqui está um exemplo em que um thread segurando o bloqueio inicia outro thread que deseja o mesmo bloqueio e, em seguida, o iniciador espera até que o início termine ... para 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();
    }
}
Victor Sorokin
fonte
0

Aqui está um exemplo:

dois threads estão em execução, cada um esperando que o outro libere o bloqueio

public class ThreadClass extends 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();
            }
        }

    }


}

}

Executar isso levaria a um impasse:

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


}

}

Praveen Kumar
fonte