grp.сreate_thread(checkTheFront);
grp.create_thread(checkTheFront);
grp.сreate_thread(checkTheFront);
grp_create_thread(checkTheFront);
thr1.join();
grp.join_all();
}
Здесь необходимо отметить несколько моментов. Обратите внимание, что теперь я использую read_write_mutex.
boost::read_write_mutex rwMutex_;
При использовании мьютексов чтения/записи блокировки тоже выполняются иначе. В примере 12.3, когда мне нужно заблокировать Queue для записи, я создаю объект класса scoped_write_lock.
boost::read_write_mutex::scoped_write_lock writeLock(rwMutex_);
А когда мне просто требуется прочитать Queue, я использую scoped_read_lock.
boost::read_write_mutex::scoped_read_lock readLock(rwMutex_);
Блокировки чтения/записи удобны, но они не предохраняют вас от серьезных ошибок. На этапе компиляции не делается проверка ресурса, представленного мьютексом rwMutex_, гарантирующая отсутствие изменения ресурса при блокировке только для чтения. Вы сами должны позаботиться о том, чтобы поток мог модифицировать состояние объекта только при блокировке для записи, поскольку компилятор это не будет делать.
Точная последовательность выполнения блокировок определяется политикой их планирования; эту политику вы задаете при конструировании объекта mutex. В библиотеке Boost Threads предусматривается четыре политики.
reader_priority
Потоки, ожидающие выполнения блокировки для чтения, ее получат раньше потоков, ожидающих выполнения блокировки для записи.
writer_priority
Потоки, ожидающие выполнения блокировки для записи, ее получат раньше потоков, ожидающих выполнения блокировки для чтения.
alternating_single_read
Чередуются блокировки для чтения и для записи. Один читающий поток получает возможность блокировки для чтения, когда подходит «очередь» читающих потоков. Эта политика в целом отдает приоритет записывающим потокам. Например, если мьютекс заблокирован для записи и имеется несколько потоков, ожидающих блокировки для чтения, а также один поток, ожидающий блокировки для записи, сначала будет выполнена одна блокировка для чтения, затем блокировка для записи и после нее — все остальные блокировки для чтения. Подразумевается, что за это время не будет новых запросов на блокировку.
alternating_many_reads
Чередуются блокировки для чтения и для записи. Выполняются все блокировки для чтения, когда подходит «очередь» читающих потоков. Другими словами, эта политика приводит к опустошению очереди всех потоков, ожидающих блокировки для чтения, в промежутке между блокировками для записи.
Каждая из этих политик имеет свои достоинства и недостатки, и их влияние будет сказываться по-разному в различных приложениях. Необходимо тщательно подойти к выбору политики, потому что если просто обеспечить приоритет для чтения или записи, это приведет к зависанию, которое я более подробно описываю ниже.
Опасности
При программировании многопоточной обработки возникает три основные проблемы: взаимная блокировка (deadlock), зависание (starvation) и состояния состязания (race conditions — условия гонок). Существуют различные по сложности методы устранения этих проблем, но их рассмотрение выходит за рамки данного рецепта. Я дам описание каждой их этих проблем, чтобы вы знали, чего следует остерегаться, но если вы планируете разработку многопоточного приложения, вам необходимо сначала выполнить некоторое предварительную работу по шаблонам многопоточной обработки.
Взаимная блокировка связана с наличием, по крайней мере, двух потоков и двух ресурсов. Пусть имеется два потока, А и В, и два ресурса, 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 может использовать блокировку, как и всякая другая функция.