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<int> p;
std::future<int> f(p.get_future())←
(1) Будущий результат f
assert(f.valid());
действителен
std::shared_future<int> sf(std::move(f));
assert(!f.valid());←
(2) f больше не действителен
assert(sf.valid());←
(3) sf теперь действителен
Здесь будущий результат f
в начальный момент действителен (1)
, потому что ссылается на асинхронное состояние обещания p
, но после передачи состояния объекту sf
результат f
оказывается недействительным (2), a sf
— действительным (3).
Как и для других перемещаемых объектов, передача владения для r-значения производится неявно, поэтому объект std::shared_future
можно сконструировать прямо из значения, возвращаемого функцией-членом get_future()
объекта std::promise
, например:
std::promise<std::string> p;←
(1) Неявная передача владения
std::shared_future<std::string> sf(p.get_future());
Здесь передача владения неявная; объект std::shared_future<>
конструируется из r-значения типа std::future<std::string>
(1).
У шаблона std::future
есть еще одна особенность, которая упрощает использование std::shared_future
совместно с новым механизмом автоматического выведения типа переменной из ее инициализатора (см. приложение А, раздел А.6). В шаблоне std::future
имеется функция-член share()
, которая создает новый объект std::shared_future
и сразу передаёт ему владение. Это позволяет сделать код короче и проще для изменения: