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

std::promise<

 std::map<SomeIndexType, SomeDataType, SomeComparator,

          SomeAllocator>::iterator> p;

auto sf = p.get_future().share();

В данном случае для sf выводится тип std::shared_future<std::map<SomeIndexType, SomeDataType, SomeComparator, SomeAllocator>::iterator>, такое название произнести-то трудно. Если компаратор или распределитель изменятся, то вам нужно будет поменять лишь тип обещания, а тип будущего результата изменится автоматически.

Иногда требуется ограничить время ожидания события — либо потому что на время исполнения некоторого участка кода наложены жесткие ограничения, либо потому что поток может заняться другой полезной работой, если событие долго не возникает. Для этого во многих функциях ожидания имеются перегруженные варианты, позволяющие задать величину таймаута.