Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Название:Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Автор:
- Жанр:
- Издательство:ДМК Пресс
- Год:2012
- Город:Москва
- ISBN:978-5-94074-448-1
- Рейтинг:
- Избранное:Добавить в избранное
-
Отзывы:
-
Ваша оценка:
Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ краткое содержание
Книга «Параллельное программирование на С++ в действии» не предполагает предварительных знаний в этой области. Вдумчиво читая ее, вы научитесь писать надежные и элегантные многопоточные программы на С++11. Вы узнаете о том, что такое потоковая модель памяти, и о том, какие средства поддержки многопоточности, в том числе запуска и синхронизации потоков, имеются в стандартной библиотеке. Попутно вы познакомитесь с различными нетривиальными проблемами программирования в условиях параллелизма.
Параллельное программирование на С++ в действии. Практика разработки многопоточных программ - читать онлайн бесплатно полную версию (весь текст целиком)
Интервал:
Закладка:
data.promise.set_value(true); ←
(6)
}
}
}
}
Функция process_connections()
повторяет цикл, пока done()
возвращает true
(1). На каждой итерации поочередно проверяется каждое соединение (2); если есть входящие данные, они читаются (3), а если в очереди имеются исходящие данные, они отсылаются (5). При этом предполагается, что в каждом входящем пакете хранится некоторый идентификатор и полезная нагрузка, содержащая собственно данные. Идентификатору сопоставляется объект std::promise
(возможно, путем поиска в ассоциативном контейнере) (4), значением которого является полезная нагрузка пакета. Исходящие пакеты просто извлекаются из очереди отправки и передаются но соединению. После завершения передачи в обещание, ассоциированное с исходящими данными, записывается значение true
, обозначающее успех (6). Насколько хорошо эта схема ложится на фактический сетевой протокол, зависит от самого протокола; в конкретном случае схема обещание/будущий результат может и не подойти, хотя структурно она аналогична поддержке асинхронного ввода/вывода в некоторых операционных системах.
В коде выше мы полностью проигнорировали возможные исключения. Хотя мир, в котором всё всегда работает правильно, был бы прекрасен, действительность не так радужна. Переполняются диски, не находятся искомые данные, отказывает сеть, «падает» база данных — всякое бывает. Если бы операция выполнялась в том потоке, которому нужен результат, программа могла бы просто сообщить об ошибке с помощью исключения. Но было бы неоправданным ограничением требовать, чтобы всё работало правильно только потому, что мы захотели воспользоваться классами std::packaged_task
или std::promise
.
Поэтому в стандартной библиотеке С++ имеется корректный способ учесть возникновение исключений в таком контексте и сохранить их как часть ассоциированного результата.
4.2.4. Сохранение исключения в будущем результате
Рассмотрим следующий коротенький фрагмент. Если передать функции square_root()
значение -1
, то она возбудит исключение, которое увидит вызывающая программа:
double square_root(double x) {
if (x<0) {
throw std::out_of_range("x<0");
}
return sqrt(x);
}
А теперь предположим, что вместо вызова square_root()
в текущем потоке
double y = square_root(-1);
мы вызываем ее асинхронно:
std::future f = std::async(square_root,-1);
double y = f.get();
В идеале хотелось бы получить точно такое же поведение: чтобы поток, вызывающий f.get()
, мог увидеть не только нормальное значение y
, но и исключение — как в однопоточной программе.
Что ж, именно так на самом деле и происходит: если функция, вызванная через std::async
, возбуждает исключение, то это исключение сохраняется в будущем результате вместо значения, а когда будущий результат оказывается готовым , вызов get()
повторно возбуждает сохраненное исключение. (Примечание: стандарт ничего не говорит о том, возбуждается ли исходное исключение или его копия; различные компиляторы и библиотеки вправе решать этот вопрос по-разному.) То же самое происходит, когда функция обернута объектом std::packaged_task
, — если при вызове задачи обернутая функция возбуждает исключение, то объект исключения сохраняется в будущем результате вместо значения, и это исключение повторно возбуждается при обращении к get()
.
Разумеется, std::promise
обеспечивает те же возможности в случае явного вызова функции. Чтобы сохранить исключение вместо значения, следует вызвать функцию-член set_exception()
, а не set_value()
. Обычно это делается в блоке catch
:
extern std::promise some_promise;
try {
some_promise.set_value(calculate_value());
} catch (...) {
some_promise.set_exception(std::current_exception());
}
Здесь мы воспользовались функцией std::current_exception()
, которая возвращает последнее возбужденное исключение, но могли вызвать std::copy_exception()
, чтобы поместить в объект-обещание новое исключение, которое никем не возбуждалось:
some_promise.set_exception(
std::copy_exception(std::logic_error("foo"));
Если тип исключения заранее известен, то это решение гораздо чище, чем использование блока try/catch
; мы не только упрощаем код, но и оставляем компилятору возможности для его оптимизации.
Есть еще один способ сохранить исключение в будущем результате: уничтожить ассоциированный с ним объект std::promise
или std::packaged_task
, не вызывая функцию установки значения в случае обещания или не обратившись к упакованной задаче. В любом случае деструктор std::promise
или std::packaged_task
сохранит в ассоциированном состоянии исключение типа std::future_error
, в котором код ошибки равен std::future_errc::broken_promise
, если только будущий результат еще не готов ; создавая объект-будущее, вы даете обещание предоставить значение или исключение, а, уничтожая объект, не задав ни того, ни другого, вы это обещание нарушаете. Если бы компилятор в этом случае не сохранил ничего в будущем результате, то ожидающие потоки могли бы никогда не выйти из состояния ожидания.
До сих пор мы во всех примерах использовали std::future
. Однако у этого шаблонного класса есть ограничения, и не в последнюю очередь тот факт, что результата может ожидать только один поток. Если требуется, чтобы одного события ждали несколько потоков, то придётся воспользоваться классом std::shared_future
.
4.2.5. Ожидание в нескольких потоках
Хотя класс std::future
сам заботится о синхронизации, необходимой для передачи данных из одного потока в другой, обращения к функциям-членам одного и того же экземпляра std::future
не синхронизированы между собой. Работа с одним объектом std::future
из нескольких потоков без дополнительной синхронизации может закончиться гонкой за данными и неопределенным поведением. Так и задумано: std::future
моделирует единоличное владение результатом асинхронного вычисления, и одноразовая природа get()
в любом случае делает параллельный доступ бессмысленным — извлечь значение может только один поток, поскольку после первого обращения к get()
никакого значения не остается.
Но если дизайн вашей фантастической параллельной программы требует, чтобы одного события могли ждать несколько потоков, то не отчаивайтесь: на этот случай предусмотрен шаблон класса std::shared_future
. Если std::future
допускает только перемещение , чтобы владение можно было передавать от одного экземпляра другому, но в каждый момент времени на асинхронный результат ссылался лишь один экземпляр, то экземпляры std::shared_future
допускают и копирование , то есть на одно и то же ассоциированное состояние могут ссылать несколько объектов.
Интервал:
Закладка: