Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Название:Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Автор:
- Жанр:
- Издательство:ДМК Пресс
- Год:2012
- Город:Москва
- ISBN:978-5-94074-448-1
- Рейтинг:
- Избранное:Добавить в избранное
-
Отзывы:
-
Ваша оценка:
Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ краткое содержание
Книга «Параллельное программирование на С++ в действии» не предполагает предварительных знаний в этой области. Вдумчиво читая ее, вы научитесь писать надежные и элегантные многопоточные программы на С++11. Вы узнаете о том, что такое потоковая модель памяти, и о том, какие средства поддержки многопоточности, в том числе запуска и синхронизации потоков, имеются в стандартной библиотеке. Попутно вы познакомитесь с различными нетривиальными проблемами программирования в условиях параллелизма.
Параллельное программирование на С++ в действии. Практика разработки многопоточных программ - читать онлайн бесплатно полную версию (весь текст целиком)
Интервал:
Закладка:
Как отмечалось во введении, стандартные атомарные типы позволяют не только избежать неопределённого поведения, связанного с гонкой за данные; они еще дают возможность задать порядок операций в потоках. Принудительное упорядочение лежит в основе таких средств защиты данных и синхронизации операций, как std::mutex
и std::future<>
. Помня об этом, перейдём к материалу, составляющему главное содержание этой главы: аспектам модели памяти, относящимся к параллелизму, и тому, как с помощью атомарных операций можно синхронизировать данные и навязать порядок доступа к памяти.
5.3. Синхронизация операций и принудительное упорядочение
Пусть имеются два потока, один из которых заполняет структуру данных, а другой читает ее. Чтобы избежать проблематичного состояния гонки, первый поток устанавливает флаг, означающий, что данные готовы, а второй не приступает к чтению данных, пока этот флаг не установлен. Описанный сценарий демонстрируется в листинге ниже.
Листинг 5.2.Запись и чтение переменной в разных потоках
#include
#include
#include
std::vector data;
std::atomic data_ready(false);
void reader_thread() {
while (!data_ready.load()) { ←
(1)
std::this_thread::sleep(std::milliseconds(1));
}
std::cout << "Ответ=" << data[0] << "\n";←
(2)
}
void writer_thread() {
data.push_back(42); ←
(3)
data_ready = true; ←
(4)
}
Оставим пока в стороне вопрос о неэффективности цикла ожидания готовности данных (1). Для работы этой программы он действительно необходим, потому что в противном случае разделение данных между потоками становится практически бесполезным: каждый элемент данных должен быть атомарным. Вы уже знаете, что неатомарные операции чтения (2)и записи (3)одних и тех же данных без принудительного упорядочения приводят к неопределённому поведению, поэтому где-то упорядочение должно производиться, иначе ничего работать не будет.
Требуемое упорядочение обеспечивают операции с переменной data_ready
типа std::atomic
и делается это благодаря отношениям происходит-раньше и синхронизируется-с , заложенным в модель памяти. Запись данных (3)происходит-раньше записи флага data_ready
(4), а чтение флага (1)происходит-раньше чтения данных (2). Когда прочитанное значение data_ready
(1)равно true
, операция записи синхронизируется-с этой операцией чтения, что приводит к порождению отношения происходит-раньше. Поскольку отношение происходит-раньше транзитивно, то запись данных (3)происходит-раньше записи флага (4), которая происходит-раньше чтения значения true
из этого флага (1), которое в свою очередь происходит-раньше чтения данных (2). И таким образом мы получаем принудительное упорядочение: запись данных происходит-раньше чтения данных, и программа работает правильно. На рис. 5.2 изображены важные отношения происходит-раньше в обоих потоках. Я включил две итерации цикла while
в потоке-читателе.

Рис. 5.2.Принудительное задание упорядочения неатомарных операций с помощью атомарных
Все это может показаться интуитивно очевидным — разумеется, операция записи значения происходит раньше операции его чтения! В случае атомарных операций по умолчанию это действительно так (на то и умолчания), однако подчеркну: у атомарных операций есть и другие возможности для задания требований к упорядочению, и скоро я о них расскажу.
Теперь, когда вы видели, как отношения происходит-раньше и синхронизируется-с работают на практике, имеет смысл поговорить о том, что же за ними стоит. Начнем с отношения синхронизируется-с.
5.3.1. Отношение синхронизируется-с
Отношение синхронизируется-с возможно только между операциями над атомарными типами. Операции над структурой данных (например, захват мьютекса) могут обеспечить это отношение, если в структуре имеются атомарные типы и определенные в ней операции выполняют необходимые атомарные операции. Однако реальным источником синхронизации всегда являются операции над атомарными типами.
Идея такова: подходящим образом помеченная атомарная операция записи W
над переменной x
синхронизируется-с подходящим образом помеченной атомарной операцией чтения над переменной x
, которая читает значение, сохраненное либо данной операцией записи ( W
), либо следующей за ней атомарной операцией записи над x
в том же потоке, который выполнил первоначальную операцию W,
либо последовательностью атомарных операций чтения-модификации-записи над x
(например, fetch_add()
или compare_exchange_weak()
) в любом потоке, при условии, что значение, прочитанное первым потоком в этой последовательности, является значением, записанным операцией W
(см. раздел 5.3.4).
Пока оставим в стороне слова «подходящим образом помеченная», потому что по умолчанию все операции над атомарными типами помечены подходящим образом. По существу сказанное выше означает ровно то, что вы ожидаете: если поток А сохраняет значение, а поток В читает это значение, то существует отношение синхронизируется-с между сохранением в потоке А и загрузкой в потоке В — как в листинге 5.2.
Уверен, вы догадались, что нюансы как раз и скрываются за словами «подходящим образом помеченная». Модель памяти в С++ допускает применение различных ограничений на упорядочение к операциям над атомарными типами, и именно это и называется пометкой. Варианты упорядочения доступа к памяти и их связь с отношением синхронизируется-с рассматриваются в разделе 5.3.3. А пока отступим на один шаг и поговорим об отношении происходит-раньше.
5.3.2. Отношение происходит-раньше
Отношение происходит-раньше — основной строительный блок механизма упорядочения операций в программе. Оно определяет, какие операции видят последствия других операций и каких именно. В однопоточной программе всё просто: если в последовательности выполняемых операций одна стоит раньше другой, то она и происходит-раньше. Иначе говоря, если операция А в исходном коде предшествует операции В, то А происходит-раньше В. Это мы видели в листинге 5.2: запись в переменную data
(3)происходит-раньше записи в переменную data_ready
(4). В общем случае между операциями, которые входят в состав одного предложения языка, нет отношения происходит-раньше, поскольку они не упорядочены. По-другому то же самое можно выразить, сказав, что порядок не определён. Мы знаем, что программа, приведённая в следующем листинге, напечатает " 1,2
" или " 2,1
", но что именно, неизвестно, потому что порядок двух обращений к get_num()
не определён.
Интервал:
Закладка: