Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Название:Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Автор:
- Жанр:
- Издательство:ДМК Пресс
- Год:2012
- Город:Москва
- ISBN:978-5-94074-448-1
- Рейтинг:
- Избранное:Добавить в избранное
-
Отзывы:
-
Ваша оценка:
Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ краткое содержание
Книга «Параллельное программирование на С++ в действии» не предполагает предварительных знаний в этой области. Вдумчиво читая ее, вы научитесь писать надежные и элегантные многопоточные программы на С++11. Вы узнаете о том, что такое потоковая модель памяти, и о том, какие средства поддержки многопоточности, в том числе запуска и синхронизации потоков, имеются в стандартной библиотеке. Попутно вы познакомитесь с различными нетривиальными проблемами программирования в условиях параллелизма.
Параллельное программирование на С++ в действии. Практика разработки многопоточных программ - читать онлайн бесплатно полную версию (весь текст целиком)
Интервал:
Закладка:
Вспомнив определение отношения межпоточно происходит раньше в разделе 5.3.2, вы обнаружите, что одно из его существенных свойств — транзитивность: если А межпоточно происходит-раньше В и В межпоточно происходит-раньше С , то А межпоточно происходит-раньше С . Это означает, что упорядочение захват-освобождение можно использовать для синхронизации данных между несколькими потоками, даже если «промежуточные» потоки на самом деле не обращались к данным.
Для рассуждений о транзитивном упорядочении нужны по меньшей мере три потока. Первый модифицирует какие-то разделяемые переменные и выполняет операцию сохранения с освобождением в одну из них. Второй читает переменную, записанную операцией сохранения с освобождением, с помощью операции загрузки с захватом и выполняет сохранение с освобождением во вторую разделяемую переменную. Наконец, третий поток выполняет операцию загрузки с захватом для второй разделяемой переменной. При условии, что операции загрузки с захватом видят значения, записанные операциями сохранения с освобождением, и тем самым поддерживают отношения синхронизируется-с, третий поток может прочитать значения других переменных, сохраненные первым потоком, даже если промежуточный поток к ним не обращался. Этот сценарий иллюстрируется в следующем листинге.
Листинг 5.9.Транзитивная синхронизация с помощью упорядочения захват-освобождение
std::atomic data[5];
std::atomic sync1(false), sync2(false);
void thread_1() {
data[0].store(42, std::memory_order_relaxed);
data[1].store(97, std::memory_order_relaxed);
data[2].store(17, std::memory_order_relaxed);
data[3].store(-141, std::memory_order_relaxed);
data[4].store(2003, std::memory_order_relaxed);←┐
Установить
sync1.store(true, std::memory_order_release);
(1)sync1
}
void thread_2()
(2)Цикл до
{ │
установки
while (!sync1.load(std::memory_order_acquire));←┘
sync1
sync2.store(true, std::memory_order_release); ←┐
Установить
}
(3) sync2
void thread_3()
(4)Цикл до
{ │
установки
while (!sync2.load(std::memory_order_acquire));←┘
sync2
assert(data[0].load(std::memory_order_relaxed) == 42);
assert(data[1].load(std::memory_order_relaxed) == 97);
assert(data[2].load(std::memory_order_relaxed) == 17);
assert(data[3].load(std::memory_order_relaxed) == -141);
assert(data[4].load(std::memory_order_relaxed) == 2003);
}
Хотя поток thread_2
обращается только к переменным sync1
(2)и sync2
(3), этого достаточно для синхронизации между thread_1
и thread_3
и, стало быть, гарантии несрабатывания утверждений assert
. Прежде всего, операции сохранения в элементы массива data
в потоке thread_1
происходят-раньше сохранения sync1
(1), потому что они связаны отношением расположено-перед в одном потоке. Поскольку операция загрузки sync1
(2)находится внутри цикла while
, она в конце концов увидит значение, сохраненное в thread_1
и, значит, образует вторую половину пары освобождение-захват. Поэтому сохранение sync1
происходит-раньше последней загрузки sync1
в цикле while
. Эта операция загрузки расположена-перед (и, значит, происходит-раньше) операцией сохранения sync2
(3), которая образует пару освобождение-захват вместе с последней операцией загрузки в цикле while
в потоке thread_3
(4). Таким образом, сохранение sync2
(3)происходит-раньше загрузки (4), которая происходит-раньше загрузок data
. В силу транзитивности отношения происходит-раньше всю эту цепочку можно соединить: операции сохранения data
происходят-раньше операций сохранения sync1
(1), которые происходят-раньше загрузки sync1
(2), которая происходит-раньше сохранения sync2
(3), которая происходит-раньше загрузки sync2
(4), которая происходит-раньше загрузок data
. Следовательно, операции сохранения data
в потоке thread_1
происходят-раньше операций загрузки data
в потоке thread_3
, и утверждения assert
сработать не могут.
В этом случае можно было бы объединить sync1
и sync2
в одну переменную, воспользовавшись операцией чтения-модификации-записи с семантикой memory_order_acq_rel
в потоке thread_2
. Один из вариантов — использовать функцию compare_exchange_strong()
, гарантирующую, что значение будет обновлено только после того, как поток thread_2
увидит результат сохранения в потоке thread_1
:
std::atomic sync(0);
void thread_1() {
// ...
sync.store(1, std::memory_order_release);
}
void thread_2() {
int expected = 1;
while (!sync.compare_exchange_strong(expected, 2,
std::memory_order_acq_rel))
expected = 1;
}
void thread_3() {
while(sync.load(std::memory_order_acquire) < 2);
// ...
}
При использовании операций чтения-модификации-записи важно выбрать нужную семантику. В данном случае нам нужна одновременно семантика захвата и освобождения, поэтому подойдет memory_order_acq_rel
, но можно было бы применить другие виды упорядочения. Операция fetch_sub
с семантикой memory_order_acquire
не синхронизируется ни с чем, хотя и сохраняет значение, потому что это не операция освобождения. Аналогично сохранение не может синхронизироваться-с операцией fetch_or
с семантикой memory_order_release
, потому что часть «чтение» fetch_or
не является операцией захвата. Операции чтения-модификации-записи с семантикой memory_order_acq_rel
ведут себя как операции захвата и освобождения одновременно, поэтому предшествующее сохранение может синхронизироваться-с такой операцией и с последующей загрузкой, как и обстоит дело в примере выше.
Если вы сочетаете операции захвата-освобождения с последовательно согласованными операциями, то последовательно согласованные операции загрузки ведут себя, как загрузки с семантикой захвата, а последовательно согласованные операции сохранения — как сохранения с семантикой освобождения. Последовательно согласованные операции чтения-модификации-записи ведут себя как операции, наделенные одновременно семантикой захвата и освобождения. Ослабленные операции так и остаются ослабленными, но связаны дополнительными отношениями синхронизируется-с и последующими отношениями происходит-раньше, наличие которых обусловлено семантикой захвата-освобождения.
Несмотря на интуитивно неочевидные результаты, всякий, кто использовал блокировки, вынужденно имел дело с вопросами упорядочения: блокировка мьютекса — это операция захвата, а его разблокировка — операция освобождения. Работая с мьютексами, вы на опыте узнаете, что при чтении значения необходимо захватывать тот же мьютекс, который захватывался при его записи. Точно так же обстоит дело и здесь — для обеспечения упорядочения операции захвата и освобождения должны применяться к одной и той же переменной. Если данные защищены мьютексом, то взаимно исключающая природа блокировки означает, что результат неотличим от того, который получился бы, если бы операции блокировки и разблокировки были последовательно согласованы. Аналогично, если для построения простой блокировки к атомарным переменным применяется упорядочение захват-освобождение, то с точки зрения программы, использующей такую блокировку, поведение кажется последовательно согласованным, хотя внутренние операции таковыми не являются.
Читать дальшеИнтервал:
Закладка: