Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Название:Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Автор:
- Жанр:
- Издательство:ДМК Пресс
- Год:2012
- Город:Москва
- ISBN:978-5-94074-448-1
- Рейтинг:
- Избранное:Добавить в избранное
-
Отзывы:
-
Ваша оценка:
Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ краткое содержание
Книга «Параллельное программирование на С++ в действии» не предполагает предварительных знаний в этой области. Вдумчиво читая ее, вы научитесь писать надежные и элегантные многопоточные программы на С++11. Вы узнаете о том, что такое потоковая модель памяти, и о том, какие средства поддержки многопоточности, в том числе запуска и синхронизации потоков, имеются в стандартной библиотеке. Попутно вы познакомитесь с различными нетривиальными проблемами программирования в условиях параллелизма.
Параллельное программирование на С++ в действии. Практика разработки многопоточных программ - читать онлайн бесплатно полную версию (весь текст целиком)
Интервал:
Закладка:
return std::shared_ptr(); ←
(5)
std::shared_ptr res(
std::make_shared(std::move(data_queue.front())));
data_queue.pop();
return res;
}
bool empty() const {
std::lock_guard lk(mut);
return data_queue.empty();
}
};
Структурно очередь в листинге 6.2 реализована аналогично стеку в листинге 6.1, отличие только в обращениях к функции data_cond.notify_one()
в push()
(1)и в наличии двух вариантов функции wait_and_pop()
(2), (3). Оба перегруженных варианта try_pop()
почти идентичны функциям pop()
в листинге 6.1 с тем отличием, что не возбуждают исключение, если очередь пуста. Вместо этого одна функция возвращает булевское значение, показывающее, были ли извлечены данные, а вторая — возвращающая указатель на данные (5)— указатель NULL
. Точно так же можно было бы поступить и в случае стека. Таким образом, если оставить в стороне функции wait_and_pop()
, то применим тот же анализ, который мы провели для стека.
Новые функции wait_and_pop()
решают проблему ожидания значения в очереди, с которой мы столкнулись при обсуждении стека; вместо того чтобы раз за разом вызывать empty()
, ожидающий поток может просто вызвать wait_and_pop()
, а структура данных обслужит этот вызов с помощью условной переменной. Обращение к data_cond.wait()
не вернет управление, пока во внутренней очереди не появится хотя бы один элемент, так что мы можем не беспокоиться но поводу того, что в этом месте кода возможна пустая очередь. При этом данные по-прежнему защищаются мьютексом. Таким образом, функции wait_and_pop()
не вводят новых состояний гонки, не создают возможности взаимоблокировок и не нарушают никаких инвариантов.
В части безопасности относительно исключений есть мелкая неприятность — если помещения данных в очередь ожидают несколько потоков, то лишь один из них будет разбужен в результате вызова data_cond.notify_one()
. Однако если этот поток возбудит исключение в wait_and_pop()
, например при конструировании std::shared_ptr<>
(4), то ни один из оставшихся потоков разбужен не будет. Если это неприемлемо, то можно заменить notify_one()
на data_cond.notify_all()
, тогда будут разбужены все потоки, но за это придётся заплатить — большая часть из них сразу же уснет снова, увидев, что очередь по-прежнему пуста. Другой вариант — включить в wait_and_pop()
обращение к notify_one()
в случае исключения, тогда другой поток сможет попытаться извлечь находящееся в очереди значение. Третий вариант — перенести инициализацию std::shared_ptr<>
в push()
и сохранять экземпляры std::shared_ptr<>
, а не сами значения данных. Тогда при копировании std::shared_ptr<>
из внутренней очереди std::queue<>
никаких исключений возникнуть не может, и wait_and_pop()
становится безопасной. В следующем листинге приведена реализация очереди, переработанная с учетом высказанных соображений.
Листинг 6.3.Потокобезопасная очередь, в которой хранятся объекты std::shared_ptr
template
class threadsafe_queue {
private:
mutable std::mutex mut;
std::queue > data_queue;
std::condition_variable data_cond;
public:
threadsafe_queue() {}
void wait_and_pop(T& value) {
std::unique_lock lk(mut);
data_cond.wait(lk, [this]{return !data_queue.empty();});
value = std::move(*data_queue.front()); ←
(1)
data_queue.pop();
}
bool try_pop(T& value) {
std::lock_guard lk(mut);
if (data_queue.empty())
return false;
value = std::move(*data_queue.front()); ←
(2)
data_queue.pop();
return true;
}
std::shared_ptr wait_and_pop() {
std::unique_lock lk(mut);
data_cond.wait(lk, [this]{return !data_queue.empty();});
std::shared_ptr res = data_queue.front(); ←
(3)
data_queue.pop();
return res;
}
std::shared_ptr try_pop() {
std::lock_guard lk(mut);
if (data_queue.empty())
return std::shared_ptr();
std::shared_ptr res = data_queue.front(); ←
(4)
data_queue.pop();
return res;
}
void push(T new_value) {
std::shared_ptr data(
std::make_shared(std::move(new_value))); ←
(5)
std::lock_guard lk(mut);
data_queue.push(data);
data_cond.notify_one();
}
bool empty() const {
std::lock_guard lk(mut);
return data_queue.empty();
}
};
Последствия хранения данных, обернутых в std::shared_ptr<>
, понятны: функции pop, которые получают значение из очереди в виде ссылки на переменную, теперь должны разыменовывать указатель (1), (2), а функции pop
, которые возвращают std::shared_ptr<>
, теперь могут напрямую извлекать его из очереди (3), (4)без дальнейших манипуляций.
У хранения данных в виде std::shared_ptr<>
есть и еще одно преимущество: выделение памяти для нового объекта можно производить не под защитой блокировки в push()
(5), тогда как в листинге 6.2 это приходилось делать в защищенном участке кода внутри pop()
. Поскольку выделение памяти, вообще говоря, дорогая операция, это изменение весьма благотворно скажется на общей производительности очереди, так как уменьшается время удержания мьютекса, а, значит, у остальных потоков остается больше времени на полезную работу.
Как и в примере стека, применение мьютекса для защиты всей структуры данных ограничивает возможности распараллеливания работы с очередью; хотя ожидать доступа к очереди могут несколько потоков, выполняющих разные функции, в каждый момент лишь один совершает какие-то действия. Однако это ограничение отчасти проистекает из того, что мы пользуемся классом std::queue<>
, — стандартный контейнер составляет единый элемент данных, который либо защищен, либо нет. Полностью взяв на себя управление деталями реализации структуры данных, мы сможем обеспечить мелкогранулярные блокировки и повысить уровень параллелизма.
6.2.3. Потокобезопасная очередь с мелкогранулярными блокировками и условными переменными
В листингах 6.2 и 6.3 имеется только один защищаемый элемент данных ( data_queue
) и, следовательно, только один мьютекс. Чтобы воспользоваться мелкогранулярными блокировками, мы должны заглянуть внутрь очереди и связать мьютекс с каждым хранящимся в ней элементом данных.
Проще всего реализовать очередь в виде односвязного списка, как показано на рис. 6.1. Указатель head направлен на первый элемент списка, и каждый элемент указывает на следующий. Когда данные извлекаются из очереди, в head записывается указатель на следующий элемент, после чего возвращается элемент, который до этого был в начале.
Добавление данных производится с другого конца. Для этого нам необходим указатель tail , направленный на последний элемент списка. Чтобы добавить узел, мы записываем в поле next в последнем элементе указатель на новый узел, после чего изменяем указатель tail , так чтобы он адресовал новый элемент. Если список пуст, то оба указателя head и tail равны NULL
.
Интервал:
Закладка: