Подтвердить что ты не робот

Стратегии блокировки и методы предотвращения взаимоблокировок в коде

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

Например, заданные потоки T1 и T2, где T1 обращается к ресурсу A, а затем B и T2 обращаются к ресурсу B, а затем A. Блокировка ресурсов в том порядке, в котором они необходимы, вызывает блокировку. Простым решением является блокировка A, а затем блокировка B, независимо от того, какой поток зависит от конкретного потока ресурсов.

Проблемная ситуация:

Thread1                         Thread2
-------                         -------
Lock Resource A                 Lock Resource B
 Do Resource A thing...          Do Resource B thing...
Lock Resource B                 Lock Resource A
 Do Resource B thing...          Do Resource A thing...

Возможное решение:

Thread1                         Thread2
-------                         -------
Lock Resource A                 Lock Resource A
Lock Resource B                 Lock Resource B
 Do Resource A thing...          Do Resource B thing...
 Do Resource B thing...          Do Resource A thing...

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

4b9b3361

Ответ 1

Техника, которую вы описываете, не просто обычна: это один из тех методов, которые, как доказано, работают все время. Есть несколько других правил, которые следует соблюдать при кодировании потокового кода на С++, среди которых наиболее важным может быть:

  • не удерживать блокировку при вызове виртуальной функции: даже если в момент написания кода вы знаете, какая функция будет вызвана и что она будет делать, код развивается и виртуальные функции должны быть переопределены, поэтому в конечном счете вы не будете знать, что он делает, и будут ли они использовать любые другие блокировки, то есть вы потеряете гарантированный порядок блокировки.
  • следить за условиями гонки: на С++ ничего не скажет вам, когда данный кусок данных делится между потоками, и вы не используете на нем какую-то синхронизацию. Один из примеров этого был опубликован в C + + Lounge в SO-чате несколько дней назад, Luc, в качестве примера этого (код в конце этого сообщения): просто пытаюсь синхронизировать что-то еще, что случается в окрестности не означает, что ваш код правильно синхронизирован.
  • попытайтесь скрыть асинхронное поведение. Обычно вы лучше скрываете свой concurrency в своей программной архитектуре, так что большинство вызывающих кодов не волнует, есть ли там нить или нет. Это упрощает работу архитектуры - особенно для тех, кто не используется для concurrency.

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

Более общий совет:

  • Не пробовайся на программировании без блокировки до тех пор, пока у вас не будет опыта параллельного программирования с помощью замков - это простой способ выбить вашу ногу или столкнуться с очень странными ошибками.
  • Уменьшить количество общих переменных и количество обращений к этим переменным до минимума.
  • Не рассчитывайте на два события, всегда встречающихся в том же порядке, даже если вы не можете видеть, как они могут изменить порядок.
  • В более общем плане: не рассчитывайте на время - не думайте, что задание всегда должно занимать определенное количество времени.

Не удалось выполнить следующий код:

#include <thread>
#include <cassert>
#include <chrono>
#include <iostream>
#include <mutex>

void
nothing_could_possibly_go_wrong()
{
    int flag = 0;

    std::condition_variable cond;
    std::mutex mutex;
    int done = 0;
    typedef std::unique_lock<std::mutex> lock;

    auto const f = [&]
    {
        if(flag == 0) ++flag;
        lock l(mutex);
        ++done;
        cond.notify_one();
    };
    std::thread threads[2] = {
        std::thread(f),
        std::thread(f)
    };
    threads[0].join();
    threads[1].join();

    lock l(mutex);
    cond.wait(l, [done] { return done == 2; });

    // surely this can't fail!
    assert( flag == 1 );
}

int
main()
{
    for(;;) nothing_could_possibly_go_wrong();
}

Ответ 2

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

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

Если вы хотите немного расширить область действия, есть способы обнаружения тупиков, когда они происходят (если по какой-то причине вы не можете разработать свою программу, чтобы их избежать) и способы взлома тупиков, когда они происходят (< например, всегда блокируя тайм-аутом, или заставляя один из тупиковых нитей иметь свою команду Lock(), или даже просто убивая один из тупиковых потоков); но я думаю, что они все уступают просто тому, чтобы убедиться, что взаимоблокировки не могут произойти в первую очередь.

(btw, если вы хотите автоматизированный способ проверить, есть ли в вашей программе возможные взаимоблокировки, проверьте инструмент valgrind helgrind, который будет отслеживать ваши шаблоны блокировки кода и уведомлять вас о любых несоответствиях - очень полезно)

Ответ 3

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

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

Простейшей отправной точкой для чтения по теме является транзакционная память.

Ответ 4

Пока вы не являетесь альтернативой известному решению, о котором вы говорите, Андрей Александреску писал о некоторых методах проверки времени компиляции, что приобретение блокировок осуществляется с помощью предполагаемых механизмов. См. http://www.informit.com/articles/article.aspx?p=25298

Ответ 5

Вы спрашиваете о уровне разработки, но я добавлю некоторые более низкие уровни, методы программирования.

  • Классифицировать каждую функцию (метод) как блокирующую, неблокирующую или имеющую неизвестное поведение блокировки.
  • Функция блокировки - это функция, которая получает блокировку или вызывает медленный системный вызов (что на практике означает, что он выполняет операции ввода-вывода) или вызывает функцию блокировки.
  • Независимо от того, гарантирована ли функция неблокирования, она является частью спецификации этой функции, как и ее предварительные условия и степень безопасности исключений. Поэтому он должен быть документирован как таковой. В Java я использую аннотацию; в С++, документированном с использованием Doxygen, я бы использовал в комментариях заголовка для этой функции форумную фразу.
  • Рассмотрите возможность вызова функции, которая не указана как неблокирующая, при этом блокировка будет опасной.
  • Восстановите такой опасный код, чтобы устранить опасность или сконцентрировать опасность на небольшой части кода (возможно, в пределах своей собственной функции).
  • Для оставшегося опасного кода предоставьте неофициальное доказательство того, что код не является действительно опасным в комментарии к коду.