Параллельное программирование на С++ в действии — страница 13 из 53

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

В стандартной библиотеке С++ такие одноразовые события моделируются с помощью будущего результата. Если поток должен ждать некоего одноразового события, то он каким-то образом получает представляющий его объект-будущее. Затем поток может периодически в течение очень короткого времени ожидать этот объект-будущее, проверяя, произошло ли событие (посмотреть на табло вылетов), а между проверками заниматься другим делом (вкушать в кафе аэропортовскую пищу по несуразным ценам). Можно поступить и иначе — выполнять другую работу до тех пор, пока не наступит момент, когда без наступления ожидаемого события двигаться дальше невозможно, и вот тогда ждать готовности будущего результата. С будущим результатом могут быть ассоциированы какие-то данные (например, номер выхода в объявлении на посадку), но это необязательно. После того как событие произошло (то есть будущий результат готов), сбросить объект-будущее в исходное состояние уже невозможно.

В стандартной библиотеке С++ есть две разновидности будущих результатов, реализованные в форме двух шаблонов классов, которые объявлены в заголовке

: уникальные будущие результаты (
std::future<>
) и разделяемые будущие результаты (
std::shared_future<>
). Эти классы устроены по образцу
std::unique_ptr
и
std::shared_ptr
. На одно событие может ссылаться только один экземпляр
std::future
, но несколько экземпляров
std::shared_future
. В последнем случае все экземпляры оказываются готовы одновременно и могут обращаться к ассоциированным с событием данным. Именно из-за ассоциированных данных будущие результаты представлены шаблонами, а не обычными классами; точно так же шаблоны
std::unique_ptr
и
std::shared_ptr
параметризованы типом ассоциированных данных. Если ассоциированных данных нет, то следует использовать специализации шаблонов
std::future
и
std::shared_future
. Хотя будущие результаты используются как механизм межпоточной коммуникации, сами по себе они не обеспечивают синхронизацию доступа. Если несколько потоков обращаются к единственному объекту-будущему, то они должны защитить доступ с помощью мьютекса или какого-либо другого механизма синхронизации, как описано в главе 3. Однако, как будет показано в разделе 4.2.5, каждый из нескольких потоков может работать с собственной копией
std::shared_future<>
безо всякой синхронизации, даже если все они ссылаются на один и тот же асинхронно получаемый результат.

Самое простое одноразовое событие — это результат вычисления, выполненного в фоновом режиме. В главе 2 мы видели, что класс

std::thread
не предоставляет средств для возврата вычисленного значения, и я обещал вернуться к этому вопросу в главе 4. Исполняю обещание.

4.2.1. Возврат значения из фоновой задачи

Допустим, вы начали какое-то длительное вычисление, которое в конечном итоге должно дать полезный результат, но пока без него можно обойтись. Быть может, вы нашли способ получить ответ на «Главный возрос жизни, Вселенной и всего на свете» из книги Дугласа Адамса[7]. Для вычисления можно запустить новый поток, но придётся самостоятельно позаботиться о передаче в основную программу результата, потому что в классе

std::thread
такой механизм не предусмотрен. Тут-то и приходит на помощь шаблон функции
std::async
(также объявленный в заголовке
).

Функция s

td::async
позволяет запустить асинхронную задачу, результат которой прямо сейчас не нужен. Но вместо объекта
std::thread
она возвращает объект
std::future
, который будет содержать возвращенное значение, когда оно станет доступно. Когда программе понадобится значение, она вызовет функцию-член
get()
объекта-будущего, и тогда поток будет приостановлен до готовности будущего результата, после чего вернет значение. В листинге ниже оказан простой пример.


Листинг 4.6. Использование

std::future
для получения результата асинхронной задачи

#include 

#include 


int find_the_answer_to_ltuae();

void do_other_stuff();


int main() {

 std::future the_answer =

  std::async(find_the_answer_to_ltuae);

 do_other_stuff();

 std::cout << "Ответ равен " << the_answer.get() << std::endl;

}

Шаблон

std::async
позволяет передать функции дополнительные параметры, точно так же, как
std::thread
. Если первым аргументом является указатель на функцию-член, то второй аргумент должен содержать объект, от имени которого эта функция-член вызывается (сам объект, указатель на него или обертывающий его
std::ref
), а все последующие аргументы передаются без изменения функции-члену. В противном случае второй и последующие аргументы передаются функции или допускающему вызов объекту, заданному в первом аргументе. Как и в
std::thread
, если аргументы представляют собой r-значения, то создаются их копии посредством перемещения оригинала. Это позволяет использовать в качестве объекта-функции и аргументов типы, допускающие только перемещение. Пример см. в листинге ниже.


Листинг 4.7. Передача аргументов функции, заданной в

std::async

#include 

#include 


struct X {

 void foo(int, std::string const&);

 std::string bar(std::string const&);

};

Вызывается

X x;                      │
p->foo(42,"hello"),

auto f1 = std::async(&X::foo, &x, 42, "hello");←┘
где p=&x

auto f2 = std::async(&X::bar, x, "goodbye");←┐
вызывается

tmpx.bar("goodbye"),

struct Y {                                   │
где tmpx — копия x

 double operator()(double);

};                               │
Вызывается tmpy(3.141),

где tmpy создается

Y y;                             │
из Y перемещающим

auto f3 = std::async(Y(), 3.141)←┘
конструктором

auto f4 = std::async(std::ref(y), 2.718);←
Вызывается y(2.718)


X baz(X&);

std::async(baz, std::ref(x); ←
Вызывается baz(x)


class move_only {

public:

 move_only();

 move_only(move_only&&);

 move_only(move_only const&) = delete;

 move_only& operator=(move_only&&);

 move_only& operator=(move_only const&) = delete;

 void operator()();                │
Вызывается tmp(), где tmp

};                                 │
конструируется с помощью

auto f5 = std::async(move_only());←┘
std::move(move_only())

По умолчанию реализации предоставлено право решать, запускает ли

std::async
новый поток или задача работает синхронно, когда программа ожидает будущего результата. В большинстве случаев такое поведение вас устроит, но можно задать требуемый режим в дополнительном параметре
std::async
перед вызываемой функцией. Этот параметр имеет тип
std::launch
и может принимать следующие значения:
std::launch::deferred
 — отложить вызов функции до того момента, когда будет вызвана функция-член
wait()
или
get()
объекта-будущего;
std::launch::async
— запускать функцию в отдельном потоке;
std::launch::deferred | std::launch::async
— оставить решение на усмотрение реализации. Последний вариант подразумевается по умолчанию. В случае отложенного вызова функция может вообще никогда не выполниться. Например:

auto f6 =                                  │
Выполнять в

 std::async(std::launch::async, Y(), 1.2);←┘
новом потоке

auto f7 =

 std::async(

  std::launch::deferred, baz, std::ref(x)); ←┐

auto f8 = std::async(                      ←┐│
Выполнять

 std::launch::deferred | std::launch::async,││
при вызове

 baz, std::ref(x));                         ││
wait() или get()

auto f9 = std::async(baz, std::ref(x));    ←┼
Оставить на

усмотрение реализации

f7.wait();←
Вызвать отложенную функцию

Ниже в этой главе и далее в главе 8 мы увидим, что с помощью

std::async
легко разбивать алгоритм на параллельно выполняемые задачи. Однако это не единственный способ ассоциировать объект
std::future
с задачей; можно также обернуть задачу объектом шаблонного класса
std::packaged_task<>
или написать код, который будет явно устанавливать значения с помощью шаблонного класса
std::promise<>
. Шаблон
std::packaged_task
является абстракцией более высокого уровня, чем
std::promise
, поэтому начнем с него.

4.2.2. Ассоциирование задачи с будущим результатом

Шаблон класса

std::packaged_task<>
связывает будущий результат с функцией или объектом, допускающим вызов. При вызове объекта
std::packaged_task<>
ассоциированная функция или допускающий вызов объект вызывается и делает будущий результат готовым, сохраняя возвращенное значение в виде ассоциированных данных. Этот механизм можно использовать для построение пулов потоков (см. главу 9) и иных схем управления, например, запускать каждую задачу в отдельном потоке или запускать их все последовательно в выделенном фоновом потоке. Если длительную операцию можно разбить на автономные подзадачи, то каждую из них можно обернуть объектом
std::packaged_task<>
и передать этот объект планировщику задач или пулу потоков. Таким образом, мы абстрагируем специфику задачи — планировщик имеет дело только с экземплярами
std::packaged_task<>
, а не с индивидуальными функциями.

Параметром шаблона класса

std::packaged_task<>
является сигнатура функции, например
void()
для функции, которая не принимает никаких параметров и не возвращает значения, или
int(std::string&, double*)
для функции, которая принимает неконстантную ссылку на
std::string
и указатель на
double
и возвращает значение типа
int
. При конструировании экземпляра
std::packaged_task
вы обязаны передать функцию или допускающий вызов объект, который принимает параметры указанных типов и возвращает значение типа, преобразуемого в указанный тип возвращаемого значения. Точного совпадения типов не требуется; можно сконструировать объект
std::packaged_task
из функции, которая принимает
int
и возвращает
float
, потому что между этими типами существуют неявные преобразования.

Тип возвращаемого значения, указанный в сигнатуре функции, определяет тип объекта

std::future<>
, возвращаемого функцией-членом
get_future()
, а заданный в сигнатуре список аргументов используется для определения сигнатуры оператора вызова в классе упакованной задачи. Например, в листинге ниже приведена часть определения класса
std::packaged_task*, int)>
.


Листинг 4.8. Определение частичной специализации

std::packaged_task

template<>

class packaged_task*, int)> {

public:

 template

 explicit packaged_task(Callable&& f);


 std::future get_future();

 void operator()(std::vector*, int);

};

Таким образом,

std::packaged_task
— допускающий вызов объект, и, значит, его можно обернуть объектом
std::function
, передать
std::thread
в качестве функции потока, передать любой другой функции, которая ожидает допускающий вызов объект, или даже вызвать напрямую. Если
std::packaged_task
вызывается как объект-функция, то аргументы, переданные оператору вызова, без изменения передаются обернутой им функции, а возвращенное значение сохраняется в виде асинхронного результата в объекте
std::future
, полученном от
get_future()
. Следовательно, мы можем обернуть задачу в
std::packaged_task
и извлечь будущий результат перед тем, как передавать объект
std::packaged_task
в то место, из которого он будет в свое время вызван. Когда результат понадобится, нужно будет подождать готовности будущего результата. В следующем примере показано, как всё это делается на практике.

Передача задач между потоками

Во многих каркасах для разработки пользовательского интерфейса требуется, чтобы интерфейс обновлялся только в специально выделенных потоках. Если какому-то другому потоку потребуется обновить интерфейс, то он должен послать сообщение одному из таких выделенных потоков, чтобы тот выполнил операцию. Шаблон

std::packaged_task
позволяет решить эту задачу, не заводя специальных сообщений для каждой относящейся к пользовательскому интерфейсу операции.


Листинг 4.9. Выполнение кода в потоке пользовательского интерфейса с применением

std::packaged_task

#include 

#include 

#include 

#include 

#include 


std::mutex m;

std::deque> tasks;


bool gui_shutdown_message_received();

void get_and_process_gui_message();


void gui_thread() {                         ←
(1)

 while (!gui_shutdown_message_received()) { ←
(2)

  get_and_process_gui_message();            ←
(3)

  std::packaged_task task; {

   std::lock_guard lk(m);

   if (tasks empty())                       ←
(4)

    continue;

   task = std::move(tasks.front());         ←
(5)

   tasks.pop_front();

  }

 task();                                    ←
(6)

 }

}


std::thread gui_bg_thread(gui_thread);


template

std::future post_task_for_gui_thread(Func f) {

 std::packaged_task task(f);       ←
(7)

 std::future res = task.get_future();←
(8)

 std::lock_guard lk(m);

 tasks.push_back(std::move(task));         ←
(9)

 return res;                               ←
(10)

}

Код очень простой: поток пользовательского интерфейса (1) повторяет цикл, пока не будет получено сообщение о необходимости завершить работу (2). На каждой итерации проверяется, есть ли готовые для обработки сообщения GUI (3), например события мыши, или новые задачи в очереди. Если задач нет (4), программа переходит на начало цикла; в противном случае извлекает задачу из очереди (5), освобождает защищающий очередь мьютекс и исполняет задачу (6). По завершении задачи будет готов ассоциированный с ней будущий результат.

Помещение задачи в очередь ничуть не сложнее: по предоставленной функции создается новая упакованная задача (7), для получения ее будущего результата вызывается функция-член

get_future()
(8), после чего задача помещается в очередь (9) еще до того, как станет доступен будущий результат (10). Затем часть программы, которая отправляла сообщение потоку пользовательского интерфейса, может дождаться будущего результата, если хочет знать, как завершилась задача, или отбросить его, если это несущественно.

В этом примере мы использовали класс

std::packaged_task
для задач, обертывающих функцию или иной допускающий вызов объект, который не принимает параметров и возвращает
void
(если он вернет что-то другое, то возвращенное значение будет отброшено). Это простейшая из всех возможных задач, но, как мы видели ранее, шаблон
std::packaged_task
применим и в более сложных ситуациях — задав другую сигнатуру функции в качестве параметра шаблона, вы сможете изменить тип возвращаемого значения (и, стало быть, тип данных, которые хранятся в состоянии, ассоциированном с будущим объектом), а также типы аргументов оператора вызова. Наш пример легко обобщается на задачи, которые должны выполняться в потоке GUI и при этом принимают аргументы и возвращают в
std::future
какие-то данные, а не только индикатор успешности завершения.

А как быть с задачами, которые нельзя выразить в виде простого вызова функции, или такими, где результат может поступать из нескольких мест? Эти проблемы решаются с помощью еще одного способа создания будущего результата: явного задания значения с помощью шаблона класса

std::promise
[8].

4.2.3. Использование
std::promise

При написании сетевых серверных программ часто возникает искушение обрабатывать каждый запрос на соединение в отдельном потоке, поскольку при такой структуре порядок коммуникации становится нагляднее и проще для программирования. Этот подход срабатывает, пока количество соединений (и, следовательно, потоков) не слишком велико. Но с ростом числа потоков увеличивается и объем потребляемых ресурсов операционной системы, а равно частота контекстных переключений (если число потоков превышает уровень аппаратного параллелизма), что негативно сказывается на производительности. В предельном случае у операционной системы могут закончиться ресурсы для запуска новых потоков, хотя пропускная способность сети еще не исчерпана. Поэтому в приложениях, обслуживающих очень большое число соединений, обычно создают совсем немного потоков (быть может, всего один), каждый из которых одновременно обрабатывает несколько запросов.

Рассмотрим один из таких потоков. Пакеты данных приходят по разным соединениям в случайном порядке, а потому и порядок помещения исходящих пакетов в очередь отправки тоже непредсказуем. Часто будет складываться ситуация, когда другие части приложения ждут либо успешной отправки данных, либо поступления нового пакета по конкретному сетевому соединению.

Шаблон

std::promise
дает возможность задать значение (типа
T
), которое впоследствии можно будет прочитать с помощью ассоциированного объекта
std::future
. Пара
std::promise
/
std::future
реализует один из возможных механизмов такого рода; ожидающий поток приостанавливается в ожидании будущего результата, тогда как поток, поставляющий данные, может с помощью
promise
установить ассоциированное значение и сделать будущий результат готовым.

Чтобы получить объект

std::future
, ассоциированный с данным обещанием
std::promise
, мы должны вызвать функцию-член
get_future()
— так же, как в случае
std::packaged_task
. После установки значения обещания (с помощью функции-члена
set_value()
) будущий результат становится готовым, и его можно использовать для получения установленного значения. Если уничтожить объект
std::promise
, не установив значение, то в будущем результате будет сохранено исключение. О передаче исключений между потоками см. раздел 4.2.4.

В листинге 4.10 приведен код потока обработки соединений, написанный по только что изложенной схеме. В данном случае для уведомления об успешной передаче блока исходящих данных применяется пара

std::promise
/
std::future
; ассоциированное с будущим результатом значение — это просто булевский флаг успех/неудача. Для входящих пакетов в качестве ассоциированных данных могла бы выступать полезная нагрузка пакета.


Листинг 4.10. Обработка нескольких соединений в одном потоке с помощью объектов-обещаний

#include 


void process_connections(connection_set& connections) {

 while(!done(connections)) {             ←
(1)

  for (connection_iterator               ←
(2)

   connection = connections.begin(), end = connections.end();

   connection != end;

   ++connection) {

   if (connection->has_incoming_data()) {←
(3)

    data_packet data = connection->incoming();

    std::promise& p =

     connection->get_promise(data.id);   ←
(4)

    p.set_value(data.payload);

   }

   if (connection->has_outgoing_data()) {←
(5)

    outgoing_packet data =

     connection->top_of_outgoing_queue();

    connection->send(data.payload);

    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
допускают и копирование, то есть на одно и то же ассоциированное состояние могут ссылать несколько объектов.

Но и функции-члены объекта

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));

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 p;←
(1) Неявная передача владения

std::shared_future sf(p.get_future());

Здесь передача владения неявная; объект

std::shared_future<>
конструируется из r-значения типа
std::future
(1).

У шаблона

std::future
есть еще одна особенность, которая упрощает использование
std::shared_future
совместно с новым механизмом автоматического выведения типа переменной из ее инициализатора (см. приложение А, раздел А.6). В шаблоне
std::future
имеется функция-член
share()
, которая создает новый объект
std::shared_future
и сразу передаёт ему владение. Это позволяет сделать код короче и проще для изменения:

std::promise<

 std::map

          SomeAllocator>::iterator> p;

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

В данном случае для

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

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

4.3. Ожидание с ограничением по времени