Есть еще один способ сохранить исключение в будущем результате: уничтожить ассоциированный с ним объект 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
допускают и копирование , то есть на одно и то же ассоциированное состояние могут ссылать несколько объектов.
Но и функции-члены объекта std::shared_future
не синхронизированы, поэтому во избежание гонки за данными при доступе к одному объекту из нескольких потоков вы сами должны обеспечить защиту. Но более предпочтительный способ — скопировать объект, так чтобы каждый поток работал со своей копией. Доступ к разделяемому асинхронному состоянию из нескольких потоков безопасен, если каждый поток обращается к этому состоянию через свой собственный объект std::shared_future
. См. Рис. 4.1.
Рис. 4.1.Использование нескольких объектов std::shared_future
, чтобы избежать гонки за данными
Одно из потенциальных применений std::shared_future
— реализация параллельных вычислений наподобие применяемых в сложных электронных таблицах: у каждой ячейки имеется единственное окончательное значение, на которое могут ссылаться формулы, хранящиеся в нескольких других ячейках. Формулы для вычисления значений в зависимых ячейках могут использовать std::shared_future
для ссылки на первую ячейку. Если формулы во всех ячейках вычисляются параллельно, то задачи, которые могут дойти до конца, дойдут, а те, что зависят от результатов вычислений других ячеек, окажутся заблокированы до разрешения зависимостей. Таким образом, система сможет но максимуму задействовать доступный аппаратный параллелизм.
Экземпляры std::shared_future
, ссылающиеся на некоторое асинхронное состояние, конструируются из экземпляров std::future
, ссылающихся на то же состояние. Поскольку объект std::future
не разделяет владение асинхронным состоянием ни с каким другим объектом, то передавать владение объекту std::shared_future
необходимо с помощью std::move
, что оставляет std::future
с пустым состоянием, как если бы он был сконструирован по умолчанию:
std::promise p;
std::future f(p.get_future())←
(1) Будущий результат f
assert(f.valid());
действителен
std::shared_future sf(std::move(f));
Читать дальше