Выбрать главу

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 и сразу передаёт ему владение. Это позволяет сделать код короче и проще для изменения: