Взаимная блокировка связана с наличием, по крайней мере, двух потоков и двух ресурсов. Пусть имеется два потока, А и В, и два ресурса, X и Y, причем поток А блокирует ресурс X, а В блокирует Y. Взаимная блокировка возникает в том случае, когда А пытается заблокировать Y, а В пытается заблокировать X. Если при работе потоков не предусмотреть какой-либо способ устранения взаимных блокировок, они будут ждать бесконечно.
Библиотека Boost Threads позволяет избегать взаимных блокировок благодаря уточнению концепций мьютекса и блокировки. Пробный мьютекс (try mutex) — это мьютекс, который используется для определения возможности блокировки путем выполнения пробной блокировки (try lock); она может быть успешной или нет, но не блокирует ресурс, а ждет момента, когда блокировка станет возможной. Применяя модели этих концепций в форме классов try_mutex и scoped_try_lock, вы можете в своей программе идти дальше и выполнять какие-то другие действия, если доступ к требуемому ресурсу заблокирован. Существует еще одно уточнение концепции пробной блокировки, называемое временной блокировкой (timed lock). Я не рассматриваю здесь подробно временные блокировки; детальное их описание вы найдете в документации библиотеки Boost Threads.
Например, в классе Queue из примера 12.2 требуется использовать мьютекс для пробной блокировки с возвратом функцией dequeue значения типа bool, показывающего, может или не может быть извлечен из очереди первый элемент. В этом случае при применении функции dequeue не приходится ждать блокировки очереди. Ниже показано, как можно переписать функцию dequeue.
bool dequeue(T& x) {
boost::try_mutex::scoped_try_lock lock(tryMutex_);
if (!lock.locked())
return(false);
else {
if (list_.empty()) throw "empty!";
x = list_.front();
list_.pop_front();
return(true);
}
}
private:
boost::try_mutex tryMutex_;
// ...
Используемые здесь мьютекс и блокировка отличаются от тех, которые применялись в примере 12.2. Убедитесь, что используемые вами имена классов мьютекса и блокировки правильно квалифицированы, в противном случае вы получите не то, на что рассчитываете.
При сериализации доступа к чему-либо вы заставляете пользователей этого ресурса выстраиваться друг за другом и дожидаться свой очереди. Если положение пользователей ресурса в очереди остается неизменным, каждый из них имеет шанс получения доступа к ресурсу. Однако если некоторым пользователям разрешается сокращать свою очередь, то до находящихся в конце очередь может никогда не дойти. Возникает зависание.
При использовании мьютекса mutex пользователи ресурса, которые находятся в состоянии ожидания, образуют группу, а не последовательность. Нельзя сказать, что существует определенный порядок между потоками, ожидающими возможности выполнения блокировки. Для мьютексов чтения/записи в библиотеке Boost Threads используется четыре политики планирования блокировок, которые были описаны ранее. Поэтому при использовании мьютексов чтения/записи необходимо понимать смысл различных политик планирования и действий ваших потоков. Если вы используете политику writer_priority и у вас много потоков, создающих блокировки для записи, ваши читающие потоки будут зависать; то же самое произойдет при применении политики reader_priority, поскольку эти политики планирования всегда отдают предпочтение одному из двух типов блокировки. Если в ходе тестирования вы понимаете, что один из типов потоков продвигается в очереди недостаточно, рассмотрите возможность перехода на применение политики alternating_many_reads или alternating_single_read. Тип политики задается при конструировании мьютекса чтения/записи.
Наконец, состояние состязания возникает в том случае, когда в программе делается предположение об определенном порядке выполнения блокировок или об их атомарности, что оказывается неверным. Например, рассмотрим пользователя класса Queue, который опрашивает первый элемент очереди и при определенном условии извлекает его из очереди с помощью функции dequeue.
if (q.getFront() == "Cyrus") {
str = q.dequeue();
// ...
Этот фрагмент программного кода хорошо работает в однопоточной среде, потому что q не может быть модифицирован в промежутке между первой и второй строкой. Однако в условиях многопоточной обработки, когда практически в любой момент другой поток может модифицировать q, следует исходить из предположения, что совместно используемые объекты модифицируются, когда поток не блокирует доступ к ним. После строки 1 другой поток, работая параллельно, может извлечь следующий элемент из q при помощи функции dequeue, что означает получение в строке 2 чего-то неожиданного или совсем ничего. Как функция getFront, так и функция dequeue блокирует один объект mutex, используемый для модификации q, но между их вызовами мьютекс разблокирован, и, если другой поток находится в ожидании выполнения блокировки, он может это сделать до того, как получит свой шанс строка 2.
Проблема состояния состязания в этом конкретном случае решается путем гарантирования сохранения блокировки на весь период выполнения операции. Создайте функцию-член dequeueIfEquals, которая извлекает следующий объект из очереди, если он равен аргументу. Функция dequeueIfEquals может использовать блокировку, как и всякая другая функция.
T dequeueIfEquals(const T& t) {
boost::mutex::scoped_lock lock(mutex_);
if (list_.front() == t)
// ...
Существуют состояния состязания другого типа, но этот пример должен дать общее представление о том, чего следует остерегаться. По мере увеличения количества потоков и совместно используемых ресурсов состояния состязания оказываются более изощренными и обнаруживать их сложнее. Поэтому следует быть особенно осторожным на этапе проектирования, чтобы не допускать их.
В многопоточной обработке самое сложное — гарантировать сериализованный доступ к ресурсам, потому что если это сделано неправильно, отладка становится кошмаром. Поскольку многопоточная программа по своей сути недетерминирована (так как потоки могут выполняться в различной очередности и с различными квантами времени при каждом новом выполнении программы), очень трудно точно обнаружить место и способ ошибочной модификации чего-либо. Здесь еще в большей степени, чем в однопоточном программировании, надежный проект позволяет минимизировать затраты на отладку и переработку.
12.3. Уведомление одного потока другим
Используется шаблон, в котором один поток (или группа потоков) выполняет какие-то действия, и требуется сделать так, чтобы об этом узнал другой поток (или группа потоков). Может использоваться главный поток, который передает работу подчиненным потокам, или может использоваться одна группа потоков для пополнения очереди и другая для удаления данных из очереди и выполнения чего-либо полезного.