Простые примеры мертвой блокировки

Простым (и возможно злоупотребил) пример RAII является класс Файла. Без RAII мог бы выглядеть примерно так код:

File file("/path/to/file");
// Do stuff with file
file.close();

, Другими словами, мы должны удостовериться, что закрываем файл, как только мы закончили с ним. Это имеет два недостатка - во-первых, везде, где мы используем Файл, мы будем иметь на названный File::close () - если мы забудем делать это, мы держим на файл дольше, чем мы должны. Вторая проблема что, если исключение выдается, прежде чем мы закроем файл?

Java решает вторую проблему с помощью наконец пункт:

try {
    File file = new File("/path/to/file");
    // Do stuff with file
} finally {
    file.close();
}

или начиная с Java 7, оператора попытки с ресурсом:

try (File file = new File("/path/to/file")) {
   // Do stuff with file
}

C++ решает обе проблемы с помощью RAII - то есть, закрывая файл в деструкторе Файла. Пока объект Файла уничтожается в нужное время (которым это должно быть так или иначе), закрытие файла заботится о для нас. Так, наш код теперь смотрит что-то как:

File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us

Это не может быть сделано в Java, так как нет никакой гарантии, когда объект будет уничтожен, таким образом, мы не сможем гарантировать, когда ресурс, такой как файл будет освобожден.

На интеллектуальные указатели - много времени, мы просто создаем объекты на стеке. Например (и кража примера из другого ответа):

void foo() {
    std::string str;
    // Do cool things to or using str
}

Это хорошо работает - но что, если мы хотим возвратить str? Мы могли записать это:

std::string foo() {
    std::string str;
    // Do cool things to or using str
    return str;
}

Так, что случилось с этим? Ну, тип возврата является станд.:: строка - таким образом, это означает, что мы возвращаемся значением. Это означает, что мы копируем str и на самом деле возвращаем копию. Это может быть дорого, и мы могли бы хотеть избежать стоимости копирования его. Поэтому мы могли бы придумать идею возвратиться ссылкой или указателем.

std::string* foo() {
    std::string str;
    // Do cool things to or using str
    return &str;
}

, К сожалению, этот код не работает. Мы возвращаем указатель на str - но str был создан на стеке, таким образом, мы быть удаленным, как только мы выходим из нечто (). Другими словами, к тому времени, когда вызывающая сторона получает указатель, это бесполезно (и возможно хуже, чем бесполезный начиная с использования, это могло вызвать все виды броских ошибок)

Так, каково решение? Мы могли создать str на "куче", использующей новый - тот путь, когда нечто () будет завершено, str не будет уничтожен.

std::string* foo() {
    std::string* str = new std::string();
    // Do cool things to or using str
    return str;
}

, Конечно, это решение не прекрасно также. Причина состоит в том, что мы создали str, но мы никогда не удаляем его. Это не могло бы быть проблемой в очень небольшой программе, но в целом, мы хотим удостовериться, что мы удаляем его. Мы могли просто сказать, что вызывающая сторона должна удалить объект, как только он закончен с ним. Оборотная сторона - то, что вызывающая сторона должна управлять памятью, которая добавляет дополнительную сложность и могла бы понять ее превратно, ведя к утечке памяти т.е. не удалив объект даже при том, что она больше не требуется.

Это - то, где интеллектуальные указатели входят. Следующий пример использует shared_ptr - я предлагаю, чтобы Вы посмотрели на различные типы интеллектуальных указателей для изучения то, что Вы на самом деле хотите использовать.

shared_ptr<std::string> foo() {
    shared_ptr<std::string> str = new std::string();
    // Do cool things to or using str
    return str;
}

Теперь, shared_ptr будет считать количество ссылок на str. Например

shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;

Теперь существует две ссылки на ту же строку. Однажды нет никаких остающихся ссылок на str, он будет удален. По сути, Вы больше не должны волноваться об удалении его сами.

Быстрое редактирование: как некоторые комментарии указали, этот пример не идеально подходит для (по крайней мере!) две причины. Во-первых, из-за реализации строк, копируя строку имеет тенденцию быть недорогим. Во-вторых, из-за то, что известно как именованная оптимизация возвращаемого значения, возвращающаяся значением, не может быть дорогим, так как компилятор может сделать некоторый ум для ускорения вещей.

Так, давайте попробуем различный пример с помощью нашего класса Файла.

Скажем, мы хотим использовать файл в качестве журнала. Это означает, что мы хотим открыться, наш файл в добавляют только режим:

File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log

Теперь, давайте установим наш файл как журнал для нескольких других объектов:

void setLog(const Foo & foo, const Bar & bar) {
    File file("/path/to/file", File::append);
    foo.setLogFile(file);
    bar.setLogFile(file);
}

, К сожалению, этот пример заканчивается ужасно - файл будет закрыт, как только этот метод заканчивается, означая, что нечто и панель теперь имеют недопустимый файл журнала. Мы могли создать файл на "куче" и передать указатель на файл и к нечто и к панели:

void setLog(const Foo & foo, const Bar & bar) {
    File* file = new File("/path/to/file", File::append);
    foo.setLogFile(file);
    bar.setLogFile(file);
}

, Но тогда кто ответственен за удаление файла? Если ни один не удаляет файл, то у нас есть и утечка памяти и утечка ресурсов. Мы не знаем или нечто, или панель закончится с файлом сначала, таким образом, мы не сможем ожидать, что любой удалит файл самостоятельно. Например, если нечто удаляет файл, прежде чем панель закончилась с ним, панель теперь имеет недопустимый указатель.

Так, как Вы, возможно, предположили, мы могли использовать интеллектуальные указатели для выручения нас.

void setLog(const Foo & foo, const Bar & bar) {
    shared_ptr<File> file = new File("/path/to/file", File::append);
    foo.setLogFile(file);
    bar.setLogFile(file);
}

Теперь, никто не должен волноваться об удалении файла - как только и нечто и панель закончили и больше не имеют ссылок на файл (вероятно, из-за нечто и уничтожаемой панели), файл будет автоматически удален.

88
задан 5 revs, 3 users 67% 8 December 2015 в 21:44
поделиться

8 ответов

Может быть, простая ситуация с банком.

class Account {
  double balance;

  void withdraw(double amount){
     balance -= amount;
  } 

  void deposit(double amount){
     balance += amount;
  } 

   void transfer(Account from, Account to, double amount){
        sync(from);
        sync(to);

        from.withdraw(amount);
        to.deposit(amount);

        release(to);
        release(from);
    }

}

Очевидно, должна быть два потока, которые пытаются запустить передачу ( a, b ) и передачу ( b, a ) одновременно, то возникнет тупик, потому что они пытаются получить ресурсы в обратном порядке.

Этот код также отлично подходит для поиска решений для выхода из тупика. Надеюсь, это поможет!

134
ответ дан 24 November 2019 в 07:23
поделиться

Вот простая тупиковая ситуация в C #.

void UpdateLabel(string text) {
   lock(this) {
      if(MyLabel.InvokeNeeded) {
        IAsyncResult res =  MyLable.BeginInvoke(delegate() {
             MyLable.Text = text;
            });
         MyLabel.EndInvoke(res);
        } else {
             MyLable.Text = text;
        }
    }
}

Если однажды вы вызовете это из потока графического интерфейса пользователя, а другой поток также вызовет это, вы можете зайти в тупик. Другой поток переходит к EndInvoke, ожидая, пока поток графического интерфейса выполнит делегат, удерживая замок. Поток GUI блокируется на той же блокировке, ожидая, пока другой поток освободит его, чего не будет, потому что поток GUI никогда не будет доступен для выполнения делегата, которого ждет другой поток. (конечно, блокировка здесь не является строго необходимой - и, возможно, не EndInvoke, но в немного более сложном сценарии блокировка может быть получена вызывающим пользователем по другим причинам, что приведет к тому же тупику.)

1
ответ дан 24 November 2019 в 07:23
поделиться

Проблема производителей-потребителей вместе с проблемой философов-обедающих, вероятно, настолько проста, насколько это возможно. Он также имеет некоторый псевдокод, который это иллюстрирует. Если они слишком сложны для новичка, им лучше постараться понять их.

1
ответ дан 24 November 2019 в 07:23
поделиться

Я считаю задачу «Обеденные философы» одним из наиболее простых примеров показа тупиковых ситуаций, поскольку четыре требования тупиковых ситуаций можно легко проиллюстрировать с помощью рисунка (особенно циклического ожидания).

Я считаю, что примеры из реального мира намного больше сбивают с толку новичка, хотя сейчас я не могу придумать хороший сценарий из реального мира (я относительно неопытен с параллелизмом в реальном мире).

3
ответ дан 24 November 2019 в 07:23
поделиться

См. Мой ответ на этот вопрос . Итог, когда двум потокам необходимо получить два разных ресурса и делать это в разном порядке, вы можете получить взаимоблокировки.

5
ответ дан 24 November 2019 в 07:23
поделиться

Вот пример кода из факультета информатики тайваньского университета , показывающий простой пример Java с блокировкой ресурсов. Это очень актуально для меня. Код ниже:

/**
 * Adapted from The Java Tutorial
 * Second Edition by Campione, M. and
 * Walrath, K.Addison-Wesley 1998
 */

/**
 * This is a demonstration of how NOT to write multi-threaded programs.
 * It is a program that purposely causes deadlock between two threads that
 * are both trying to acquire locks for the same two resources.
 * To avoid this sort of deadlock when locking multiple resources, all threads
 * should always acquire their locks in the same order.
 **/
public class Deadlock {
  public static void main(String[] args){
    //These are the two resource objects 
    //we'll try to get locks for
    final Object resource1 = "resource1";
    final Object resource2 = "resource2";
    //Here's the first thread.
    //It tries to lock resource1 then resource2
    Thread t1 = new Thread() {
      public void run() {
        //Lock resource 1
        synchronized(resource1){
          System.out.println("Thread 1: locked resource 1");
          //Pause for a bit, simulating some file I/O or 
          //something. Basically, we just want to give the 
          //other thread a chance to run. Threads and deadlock
          //are asynchronous things, but we're trying to force 
          //deadlock to happen here...
          try{ 
            Thread.sleep(50); 
          } catch (InterruptedException e) {}

          //Now wait 'till we can get a lock on resource 2
          synchronized(resource2){
            System.out.println("Thread 1: locked resource 2");
          }
        }
      }
    };

    //Here's the second thread.  
    //It tries to lock resource2 then resource1
    Thread t2 = new Thread(){
      public void run(){
        //This thread locks resource 2 right away
        synchronized(resource2){
          System.out.println("Thread 2: locked resource 2");
          //Then it pauses, for the same reason as the first 
          //thread does
          try{
            Thread.sleep(50); 
          } catch (InterruptedException e){}

          //Then it tries to lock resource1.  
          //But wait!  Thread 1 locked resource1, and 
          //won't release it till it gets a lock on resource2.  
          //This thread holds the lock on resource2, and won't
          //release it till it gets resource1.  
          //We're at an impasse. Neither thread can run, 
          //and the program freezes up.
          synchronized(resource1){
            System.out.println("Thread 2: locked resource 1");
          }
        }
      }
    };

    //Start the two threads. 
    //If all goes as planned, deadlock will occur, 
    //and the program will never exit.
    t1.start(); 
    t2.start();
  }
}
53
ответ дан 24 November 2019 в 07:23
поделиться

Пусть природа объяснит тупик,

Тупик: Лягушка против Змеи

«Я бы хотел, чтобы они ушли их пути разные, но я был истощены », - сказал фотограф. «Лягушка все время пыталась оторвать змею, но змея просто не отпускала» .

enter image description here

57
ответ дан 24 November 2019 в 07:23
поделиться

Выберите упрощенный сценарий, в котором может возникнуть тупик при ознакомлении с концепцией ваших студентов. Это потребует минимум двух потоков и минимум двух ресурсов (я думаю). Цель состоит в том, чтобы разработать сценарий, в котором первый поток имеет блокировку на ресурсе один и ожидает освобождения блокировки ресурса два, в то время как в то же время поток два удерживает блокировку на ресурсе два и ожидает блокировка ресурса, который нужно снять.

На самом деле не имеет значения, каковы основные ресурсы; для простоты вы можете просто сделать их парой файлов, в которые оба потока могут писать.

РЕДАКТИРОВАТЬ: Это предполагает отсутствие взаимодействия между процессами, кроме удерживаемых блокировок.

1
ответ дан 24 November 2019 в 07:23
поделиться
Другие вопросы по тегам:

Похожие вопросы: