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

#c #multithreading #concurrency #thread-safety #mutex

#c #шаблоны проектирования #блокировка #взаимоблокировка

Вопрос:

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

Например, даны потоки 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...
  

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

Ответ №1:

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

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

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

Более общие рекомендации:

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

Следующий код завершится ошибкой:

 #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 = [amp;]
    {
        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();
}
  

Комментарии:

1. «вы не будете знать, что он делает и будет ли он принимать какие-либо другие блокировки», если спецификация функции базового класса не заключается в том, что метод никогда не получает блокировку.

2. «эта спецификация должна была бы каким-то образом применяться»: можно было бы утверждать то же самое о любом аспекте спецификации. На практике вряд ли какие-либо из них применяются . Некоторые проверяются путем проверки, большинство проверяется тестированием.

3. @Raedwald Я хотел сказать, что вы не всегда можете полагаться на спецификацию функции базового класса, чтобы отразить реальность того, что делает функция (и.или другие версии функции делают). большинство из них проверяется путем проверки правильности, и, к сожалению, это означает, что большинство проблем не устраняются вовремя, чтобы предотвратить повреждение. Это не означает, что функция базового класса не должна быть четко указана — это просто означает, что на такие спецификации на практике нельзя обязательно полагаться.

Ответ №2:

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

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

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

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

Комментарии:

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

2. @rlc и @Jeremy: И «безблокировочное» программирование на самом деле даже не безблокировочно. Это просто использование блокировки, реализованной в аппаратном обеспечении, в протоколах согласованности кэша и межпроцессорной связи. Без какой-либо блокировки столь любимые инструкции по обмену сравнениями не могли бы работать.

3. @rlc достаточно справедливо — но я не утверждал иначе, я только сказал, что это связанная техника. @Zan он не заблокирован в том смысле, что поток гарантированно не блокируется на неопределенный срок, и, следовательно, вероятность возникновения взаимоблокировки отсутствует.

4. @ZanLynx верно: процессоры будут синхронизироваться друг с другом, и в архитектуре x86 утверждается вывод БЛОКИРОВКИ, оба из которых требуют значительных затрат. Сами блокировки реализуются с использованием тех же механизмов синхронизации аппаратного уровня, хотя и на программном уровне, хотя есть точки синхронизации, блокировок нет в том смысле, что по крайней мере один поток всегда будет продвигаться вперед. В Википедии есть хорошее определение здесь

Ответ №3:

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

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

Упрощенной отправной точкой для чтения по этому вопросу является транзакционная память.

Ответ №4:

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

Ответ №5:

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

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