Такой подход поначалу может показаться немного странным: а почему бы мьютексу mutex не иметь методы lock и unlock? Применение класса scoped_lock, который обеспечивает блокировку при конструировании и разблокировку при уничтожении, на самом деле более удобно и менее подвержено ошибкам. Когда вы создаете блокировку, используя scoped_lock, мьютекс блокируется на весь период существования объекта scoped_lock, т.е. вам не надо ничего разблокировать в явной форме на каждой ветви вычислений. С другой стороны, если вам приходится явно разблокировать захваченный мьютекс, необходимо гарантировать перехват любых исключений, которые могут быть выброшены в вашей функции (или где-нибудь выше ее в стеке вызовов), и гарантировать разблокировку mutex. При использовании scoped_lock, если выбрасывается исключение или функция возвращает управление, объект scoped_lock автоматически уничтожается и mutex разблокируется.
Использование мьютекса позволяет сделать всю работу, однако хочется немного большего. При таком подходе нет различия между чтением и записью, что существенно, так как неэффективно заставлять потоки ждать в очереди доступа к ресурсу, когда многие из них выполняют только операции чтения, для которых не требуется монопольный доступ. Для этого в библиотеке Boost Threads предусмотрен класс read_write_mutex. Пример 12.3 показывает, как можно реализовать пример 12.2, используя read_write_mutex с функцией-членом front, которая позволяет вызывающей программе получить копию первого элемента очереди без его выталкивания.
Пример 12.3. Использование мьютекса чтения/записи
#include <iostream>
#include <boost/thread/thread.hpp>
#include <boost/thread/read_write_mutex.hpp>
#include <string>
template<typename T>
class Queue {
public:
Queue() : // Использовать мьютекс чтения/записи и придать ему приоритет
// записи
rwMutex_(boost::read_write_scheduling_policy::writer_priority) {}
~Queue() {}
void enqueue(const T& x) {
// Использовать блокировку чтения/записи, поскольку enqueue
// обновляет состояние
boost::read_write_mutex::scoped_write_lock writeLock(rwMutex_);
list_.push_back(x);
}
T dequeue() {
// Снова использовать блокировку для записи
boost::read_write_mutex::scoped_write_lock writeLock(rwMutex_);
if (list_.empty())
throw "empty!";
T tmp = list_.front();
list_.pop_front();
return(tmp);
}
T getFront() {
// Это операция чтения, поэтому требуется блокировка только для чтения
boost::read_write_mutex::scoped_read_lock.readLock(rwMutex_);
if (list_.empty())
throw "empty!";
return(list_.front());
}
private:
std::list<T> list_;
boost::read_write_mutex rwMutex_;
};
Queue<std::string> queueOfStrings;
void sendSomething() {
std::string s;
for (int i = 0, i < 10; ++i) {
queueOfStrings.enqueue("Cyrus");
}
}
void checkTheFront() {
std::string s;
for (int i=0; i < 10; ++i) {
try {
s = queueOfStrings.getFront();
} catch(...) {}
}
}
int main() {
boost::thread thr1(sendSomething);
boost::thread_group grp;
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 — условия гонок). Существуют различные по сложности методы устранения этих проблем, но их рассмотрение выходит за рамки данного рецепта. Я дам описание каждой их этих проблем, чтобы вы знали, чего следует остерегаться, но если вы планируете разработку многопоточного приложения, вам необходимо сначала выполнить некоторое предварительную работу по шаблонам многопоточной обработки.