Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Название:Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Автор:
- Жанр:
- Издательство:ДМК Пресс
- Год:2012
- Город:Москва
- ISBN:978-5-94074-448-1
- Рейтинг:
- Избранное:Добавить в избранное
-
Отзывы:
-
Ваша оценка:
Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ краткое содержание
Книга «Параллельное программирование на С++ в действии» не предполагает предварительных знаний в этой области. Вдумчиво читая ее, вы научитесь писать надежные и элегантные многопоточные программы на С++11. Вы узнаете о том, что такое потоковая модель памяти, и о том, какие средства поддержки многопоточности, в том числе запуска и синхронизации потоков, имеются в стандартной библиотеке. Попутно вы познакомитесь с различными нетривиальными проблемами программирования в условиях параллелизма.
Параллельное программирование на С++ в действии. Практика разработки многопоточных программ - читать онлайн бесплатно полную версию (весь текст целиком)
Интервал:
Закладка:
void push(T const& data) {
counted_node_ptr new_node;
new_node.ptr = new node(data);
new_node.external_count = 1;
new_node.ptr->next = head.load(std::memory_order_relaxed);
while (!head.compare_exchange_weak(
new_node.ptr->next, new_node,
std::memory_order_release, std::memory_order_relaxed));
}
А что можно сказать о коде pop()
? Чтобы получить желаемое отношение происходит-раньше, перед доступом к next
необходима операция с семантикой std::memory_order_acquire
или более сильной. Указатель, который разыменовывается для доступа к полю next
, — это прежнее значение, прочитанное операцией compare_exchange_strong()
в increase_head_count()
, поэтому указанная семантика нужна в случае успеха. Как и в push()
, если обмен закончился неудачно, мы просто повторяем цикл, поэтому для отказа можно задать ослабленное упорядочение:
void increase_head_count(counted_node_ptr& old_counter) {
counted_node_ptr new_counter;
do {
new_counter = old_counter;
++new_counter.external_count;
}
while (!head.compare_exchange_strong(
old_counter, new_counter,
std::memory_order_acquire, std::memory_order_relaxed));
old_counter.external_count = new_counter.external_count;
}
Если вызов compare_exchange_strong()
завершается успешно, то мы знаем, что раньше в поле ptr
прочитанного значения находилось то, что теперь хранится в переменной old_counter
. Поскольку сохранение в push()
было операцией освобождения, а данный вызов compare_exchange_strong()
— операция захвата, то сохранение синхронизируется-с загрузкой, и мы имеем отношение происходит-раньше. Следовательно, сохранение в поле ptr
в push()
происходит-раньше доступа к ptr->next
в pop()
, и мы в безопасности.
Отметим, что для этого анализа порядок доступа к памяти в начальном вызове head.load()
не имел значения, поэтому в нем безопасно задать семантику std::memory_order_relaxed
.
Далее на очереди операция compare_exchange_strong()
, которая записывает в head
значение old_head.ptr->next
. Нужно ли наложить на нее какие-нибудь ограничения, чтобы гарантировать целостность данных в этом потоке? Если обмен завершается успешно, то мы обращаемся к ptr->data
, поэтому должны быть уверены, что сохранение ptr->data
в потоке, выполняющем push()
, происходит-раньше загрузки. Но такая уверенность уже есть: операция захвата в increase_head_count()
гарантирует, что существует отношение синхронизируется-с между сохранением в потоке, выполняющем push()
, и операцией сравнения с обменом. Поскольку сохранение данных в потоке, выполняющем push()
, расположено перед сохранением head
, а вызов increase_head_count()
расположен перед загрузкой ptr->data
, то отношение происходит-раньше имеет место, и всё будет хорошо, даже если для операции сравнения с обменом в pop()
задана семантика std::memory_order_relaxed
. Есть еще всего одно место, где изменяется ptr->data
— тот самый вызов swap()
, на который вы сейчас смотрите, и ни один другой поток не может оперировать тем же узлом — в этом и заключается смысл сравнения с обменом.
Если compare_exchange_strong()
завершается неудачно, то к новому значению old_head
не будет обращений до следующей итерации цикла, и, поскольку мы уже решили, что семантики std::memory_order_acquire
хватало в increase_head_count()
, то здесь будет достаточно std::memory_order_relaxed
.
Что можно сказать насчёт других потоков? Нужны ли более сильные ограничения, чтобы и другие потоки работали безопасно? Нет, не нужны, потому что head
модифицируется только операциями сравнения с обменом. Будучи операциями чтения-модификации-записи, они составляют часть последовательности освобождений, начатой операцией сравнения с обменом в push()
. Поэтому compare_exchange_weak()
в push()
синхронизируется-с операцией compare_exchange_strong()
в increase_head_count()
, которая прочитает сохраненное значение, даже если в промежутке другие потоки изменят head
.
Мы почти закончили, осталось только рассмотреть функции, в которых используются операции fetch_add()
, изменяющие счетчик ссылок. Поток, который добрался до возврата данных из узла, может продолжать в твердой уверенности, что никакой другой поток не сможет модифицировать хранящиеся в узле данные. Однако любой поток, который потерпел неудачу при извлечении данных, знает, что какой-то другой поток данные в узле модифицировал ; он использовал функцию swap()
для извлечения данных. Следовательно, чтобы предотвратить гонку за данными мы должны гарантировать, что swap()
происходит-раньше delete
. Чтобы добиться этого, проще всего задать семантику std::memory_order_release
при вызове fetch_add()
в ветви, где мы возвращаем данные, и семантику std::memory_order_acquire
— в ветви, где мы возвращаемся в начало цикла. Однако даже это перебор — лишь один поток выполняет delete
(тот, что сбросил счетчик в нуль), поэтому только этому потоку нужно выполнить операцию захвата. К счастью, поскольку fetch_add()
— операция чтения-модификации-записи, то она составляет часть последовательности освобождений, поэтому для достижения цели нам достаточно дополнительной операции load()
. Если в ветви, где происходит возврат в начало цикла, счетчик ссылок уменьшается до нуля, то здесь же можно перезагрузить счетчик ссылок с семантикой std::memory_order_acquire
, чтобы обеспечить требуемое отношение синхронизируется-с, а в самой операции fetch_add()
достаточно задать std::memory_order_relaxed
. Окончательная реализация стека с новой версией pop()
приведена ниже.
Листинг 7.12.Свободный от блокировок стек с подсчётом ссылок и ослабленными атомарными операциями
template
class lock_free_stack {
private:
struct node;
struct counted_node_ptr {
int external_count;
node* ptr;
};
struct node {
std::shared_ptr data;
std::atomic internal_count;
counted_node_ptr next;
node(T const& data_):
data(std::make_shared(data_)), internal_count(0) {}
};
std::atomic head;
void increase_head_count(counted_node_ptr& old_counter) {
counted_node_ptr new_counter;
do {
new_counter = old_counter;
++new_counter.external_count;
}
while (!head.compare_exchange_strong(old_counter, new_counter,
std::memory_order_acquire,
std::memory_order_relaxed));
old_counter.external_count = new_counter.external_count;
}
public:
~lock_free_stack() {
while(pop());
}
void push(T const& data) {
counted_node_ptr new_node;
new_node.ptr = new node(data);
new_node.external_count = 1;
new_node.ptr->next = head.load(std::memory_order_relaxed);
while (!head.compare_exchange_weak(
new_node.ptr->next, new_node,
std::memory_order_release,
std::memory_order_relaxed));
}
std::shared_ptr pop() {
counted_node_ptr old_head =
head.load(std::memory_order_relaxed);
for (;;) {
increase_head_count(old_head);
node* const ptr = old_head.ptr;
Интервал:
Закладка: