Напишіть програму, яка напевно зайде в глухий кут [закрито]


86

Нещодавно це запитання я задав в інтерв’ю.

Я відповів, що глухий кут відбувається, якщо чергування піде не так, але інтерв'юер наполягав на тому, що програма, яка завжди впаде в глухий кут, незалежно від чередування, може бути написана.

Чи можемо ми написати таку програму? Чи можете ви вказати мені на такий приклад програми?


3
Інтерв'юер, безумовно, дурний хлопець.
Лев

23
Інтерв'юер, звичайно, не дурень. Повне розуміння предмета означає, що ви повинні вміти пояснити випадки полярних країв: зробити програму ніколи не блокувати і завжди блокувати.
Юрій Зубарев

Відповіді:


100

ОНОВЛЕННЯ: Це питання було предметом мого блогу в січні 2013 року . Дякую за чудове запитання!


Як ми можемо написати програму, яка завжди впаде в глухий кут, незалежно від того, як заплановані потоки?

Ось приклад у C #. Зверніть увагу, що програма, здається, не містить блокування та спільних даних. У ньому є лише одна локальна змінна та три твердження, і тим не менш, вона забивається із 100% впевненістю. Було б важко придумати простішу програму, яка б із певністю зайшла у глухий кут.

Вправа для читача №1: поясніть, як це зайшло в глухий кут. (Відповідь - у коментарях.)

Вправа для читача №2: продемонструйте той самий глухий кут у Java. (Відповідь тут: 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
Мої знання теоретичного C # обмежені, але я припускаю, що завантажувач класів гарантує, що код запускається однопотоково, як це робиться в Java. Я майже впевнений, що подібний приклад є у Java Puzzlers.
Voo

11
@Voo: У вас гарна пам’ять. Ніл Гафтер - співавтор "Java Puzzlers" - і ми представили досить затуманену версію цього коду в нашій бесіді "C # Puzzlers" на конференції розробників в Осло кілька років тому.
Ерік Ліпперт,

41
@Lieven: Статичний конструктор повинен запускатися не більше одного разу, і він повинен запускатися перед першим викликом будь-якого статичного методу в класі. Основний - це статичний метод, тому основний потік викликає статичний ctor. Щоб гарантувати, що він працює лише один раз, CLR виймає замок, який не відпускається, поки статичний ктор не закінчиться. Коли ctor запускає новий потік, цей потік також викликає статичний метод, тому CLR намагається взяти блокування, щоб побачити, чи потрібно йому запускати ctor. Тим часом основний потік "приєднується" до заблокованого потоку, і тепер у нас є глухий кут.
Ерік Ліпперт,

33
@artbristol: Я ніколи не писав стільки, скільки рядок коду Java; Не бачу причин починати зараз.
Ерік Ліпперт,

4
О, я припускав, що у вас є відповідь на ваше вправу №2. Вітаємо з отриманням стільки голосів за відповідь на питання Java.
artbristol

27

Засувка тут гарантує, що обидва замки тримаються, коли кожна нитка намагається зафіксувати іншу:

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

   }

}

Цікаво запустити jconsole, який правильно покаже вам глухий кут на вкладці Threads.


3
Наразі це найкраще, але я б замінив його sleepна відповідну засувку: теоретично, тут ми маємо перегонові умови. Хоча ми можемо бути майже впевнені, що достатньо 0,5 секунди, це не дуже добре для завдання співбесіди.
alf

25

Тупик трапляється, коли потоки (або те, що ваша платформа називає своїми блоками виконання) отримують ресурси, де кожен ресурс може одночасно утримуватися лише одним потоком, і він утримується на цих ресурсах таким чином, що утримання не можна попереджувати, і існує деяка "кругова" взаємозв'язок між потоками, така що кожен потік у тупиковій ситуації чекає на отримання ресурсу, який утримується іншим потоком.

Отже, найпростіший спосіб уникнути глухого кута - це надати загальний порядок ресурсам і нав'язати правило, згідно з яким ресурси отримуються лише потоками в порядку . І навпаки, глухий кут можна навмисно створити, запустивши потоки, які отримують ресурси, але не отримують їх по порядку. Наприклад:

Дві нитки, два замки. Перший потік запускає цикл, який намагається отримати замки в певному порядку, другий потік запускає цикл, який намагається отримати замки в протилежному порядку. Кожна нитка звільняє обидва замки після успішного отримання замків.

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

Зараз у цьому питанні було кілька коментарів, які вказують на різницю між ймовірністю та певністю тупикової ситуації. У якомусь сенсі ця відмінність є академічним питанням. З практичної точки зору, я, безумовно, хотів би бачити працюючу систему, яка не забивається в коді, написаному мною вище :)

Однак питання співбесіди часом можуть бути академічними, і це ТА питання має в назві слово "напевно", тож далі йде програма, яка, безумовно, тупикова. Створюються два Lockerоб'єкти, кожен отримує два блокування та CountDownLatchвикористовується для синхронізації між потоками. Кожен Lockerзамикає перший замок, після чого відраховує засувку один раз. Коли обидві нитки отримали замок і відрахували засувку, вони проходять повз заслінку замку і намагаються отримати другий замок, але в кожному випадку інша нитка вже тримає бажаний замок. Ця ситуація призводить до певного глухого кута.

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
Вибачте, я цитую Лінуса: "Розмова дешева. Покажіть мені код" - це приємне завдання, і це напрочуд складніше, ніж здається.
alf

2
Запустити цей код можна без глухого кута
Володимир Жиляєв

1
Гаразд, ви, брутальні хлопці, але я думаю, що зараз це повна відповідь.
Greg Mattes

@GregMattes дякую :) Не можу додати нічого, крім +1, і сподіваюся, вам було весело :)
alf

15

Ось приклад Java, слідуючи прикладу Еріка Ліпперта:

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
Я думаю, що використання методу join in run мало вводить в оману. Це передбачає, що це інше об'єднання, крім того, що в статичному блоці, необхідне для отримання тупикової ситуації, тоді як блокування відбувається через оператор "new Lock ()". Мій переписаний, використовуючи статичний метод , як і в C # , наприклад: stackoverflow.com/a/16203272/2098232
luke657

Не могли б ви пояснити свій приклад?
gstackoverflow

за моїми експериментами t.join (); внутрішній метод run () надлишковий
gstackoverflow

Я видалив зайвий код, який заважає зрозуміти
gstackoverflow

11

Ось приклад із документації:

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 Для підключення підручника Java.
мере

4
"це надзвичайно ймовірно" недостатньо для того, щоб "напевно зайти в глухий кут"
alf

1
@alf О, але фундаментальна проблема тут досить добре продемонстрована. Можна написати планувальник Round Robin, який розкриває Object invokeAndWait(Callable task)метод. Тоді все Callable t1повинен зробити invokeAndWait()для в Callable t2протягом свого терміну служби до повернення, і навпаки.
користувач268396

2
@ user268396 ну, фундаментальне питання є тривіальним і нудним :) Вся суть завдання полягає в тому, щоб з’ясувати - або довести, що ви розумієте - що напрочуд важко отримати гарантований глухий кут (а також гарантувати що-небудь в асинхронному світі ).
alf

4
@bezz sleepнудно. Хоча я вірю, що жодна нитка не запускатиметься протягом 5 секунд, все одно це умова перегони. Ви не бажаєте наймати програміста, який би покладався на sleep()вирішення умов перегонів :)
alf

9

Я переписав Java-версію прикладу тупикової ситуації Юрія Зубарева, опублікований Еріком Ліппертом: https://stackoverflow.com/a/9286697/2098232, щоб більше нагадувати версію C #. Якщо блок ініціалізації Java працює подібно до статичного конструктора C # і спочатку отримує блокування, нам не потрібен інший потік, щоб також викликати метод join, щоб отримати глухий кут, йому потрібно лише викликати якийсь статичний метод із класу Lock, як оригінальний C # приклад. Виниклий тупик, схоже, підтверджує це.

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

}

чому, коли я коментую Lock.initialize () у методі запуску, він не блокується? метод ініціалізації нічого не робить, хоча ??
Aequitas

@Aequitas лише здогадка, але метод можна оптимізувати; не впевнений у тому, як це буде працювати з нитками
Дейв Кусіно

5

Це не найпростіше завдання на співбесіду: у моєму проекті це паралізувало роботу команди на цілий день. Зупинити програму дуже просто, але довести її до стану, коли дамп потоку пише щось на зразок, дуже важко

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)

Отже, метою було б отримати глухий кут, який JVM вважатиме тупиковим. Очевидно, що рішення не схоже

synchronized (this) {
    wait();
}

працюватиме в цьому сенсі, хоча вони справді зупиняться назавжди. Покладатись на умови перегонів теж не є хорошою ідеєю, оскільки під час співбесіди ти зазвичай хочеш показати щось доказово працює, а не те, що повинно працювати більшу частину часу.

Зараз sleep()рішення є нормальним, у певному сенсі важко уявити ситуацію, коли це не спрацьовує, але не чесно (ми займаємося чесним видом спорту, чи не так?). Рішення @artbristol (моє те саме, просто різні об'єкти як монітори) є приємним, але довгим і використовує нові примітиви одночасності, щоб привести потоки в потрібний стан, що не так вже й весело:

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

Я пам’ятаю, що synchronizedрішення -only вміщує 11..13 рядків коду (за винятком коментарів та імпорту), але все ще не пам’ятає фактичного фокусу. Оновить, якщо я це зроблю.

Оновлення: ось негарне рішення щодо 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) {
            }
        }
    }
}

Зверніть увагу, ми замінюємо засувку на монітор об’єкта (використовуємо "a"як об’єкт).


Хам, я думаю, це чесне завдання на співбесіду. Він просить вас по-справжньому зрозуміти тупикові ситуації та блокування в Java. Я не думаю, що загальна ідея така важка (переконайтесь, що обидва потоки можуть продовжуватися лише після того, як обидва заблокують свій перший ресурс), вам слід просто пам’ятати CountdownLatch - але як інтерв’юер я б допоміг співрозмовнику в цій частині якби він міг пояснити, що саме йому потрібно (це не клас, який більшості розробників коли-небудь потрібен, і ви не можете погуглити його на співбесіді). Я хотів би отримати такі цікаві питання для співбесід!
Voo

На той момент, коли ми з нею грали, @Voo у JDK не було засувок, тому все було вручну. І різниця між LOCKEDі waiting to lockнезначна, а не те, що ви читаєте під час сніданку. Але добре, ти, мабуть, маєш рацію. Дозвольте перефразувати.
alf

4

Ця версія C #, я думаю, java повинна бути досить схожою.

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

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

2
Хороший! Дійсно найкоротша програма C #, яка створює глухий кут, якщо видалити consoleоператори. Ви можете просто написати всю Mainфункцію як 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();
        }
    }
}

Програма завжди блокує ситуацію, оскільки кожен потік чекає біля перешкоди для інших потоків, але для того, щоб дочекатися перешкоди, потік повинен утримувати монітор BadRunnable.class.


3
} catch (InterruptedException ex) { continue; }... красиво
artbristol

2

Тут є приклад у Java

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

Де викрадач потрапляє в глухий кут, коли він відмовляється відмовитись від жертви, поки не отримає готівку, але учасник переговорів відмовляється відмовитись від готівки, поки не отримає жертву.


Ця реалізація не є відповідною, як зазначено. Деякі фрагменти коду відсутні. Однак загальна думка, яку ви висловлюєте, є правильною, що стосується суперечок щодо ресурсів, що ведуть до глухого кута.
Майстер-начальник

приклад педагогічний, тому мені цікаво, чому ви трактуєте його як недоречний ... відсутній код - це порожні методи, де імена методів повинні бути корисними (але не показані для стислості)
Тобі,

1

Простий пошук дав мені такий код:

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

Джерело: Тупик


3
"це надзвичайно ймовірно" недостатньо для того, щоб "обов'язково зайти в глухий кут"
alf

1

Ось приклад, коли один потік, що утримує замок, запускає інший потік, який хоче той самий замок, а потім стартер чекає, поки запущений закінчиться ... назавжди:

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

Ось приклад:

два потоки працюють, кожен чекає, поки інший звільнить блокування

публічний клас ThreadClass розширює 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();
            }
        }

    }


}

}

Запуск цього призведе до глухого кута:

публічний клас 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);


}

}

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.