Как выделяется хранилище, связанное с std::future?

Один из способов получить std::future — через std::async:

int foo()
{
  return 42;
}

...

std::future<int> x = std::async(foo);

Как в этом примере выделяется хранилище для асинхронного состояния x и какой поток (если задействовано более одного потока) отвечает за выполнение выделения? Кроме того, имеет ли клиент std::async какой-либо контроль над распределением?

Для контекста я вижу, что один из конструкторов std::promise может получить allocator, но мне непонятно, можно ли настроить выделение std::future на уровне std::async.


person Jared Hoberock    schedule 11.10.2012    source источник


Ответы (2)


Судя только по аргументам std::async, кажется, что нет никакого способа контролировать распределение внутреннего std::promise, и поэтому он может просто использовать что угодно, хотя, вероятно, std::allocator. Хотя я предполагаю, что теоретически это не указано, вполне вероятно, что общее состояние выделяется внутри вызывающего потока. Я не нашел никакой явной информации в стандарте по этому вопросу. В конце концов, std::async — это очень специализированное средство для простого асинхронного вызова, так что вам не нужно думать, есть ли вообще где-нибудь есть std::promise.

Для более прямого управления поведением асинхронного вызова также есть std::packaged_task, который действительно имеет аргумент распределителя. Но из простой стандартной цитаты не совсем ясно, используется ли этот распределитель только для выделения памяти для функции (поскольку std::packaged_task является своего рода специальным std::function) или он также используется для выделения общего состояния внутреннего std::promise, хотя кажется вероятным:

30.6.9.1 [futures.task.members]:

Эффекты: создает новый объект packaged_task с общим состоянием и инициализирует сохраненную задачу объекта с помощью std::forward<F>(f). Конструкторы, принимающие аргумент Allocator, используют его для выделения памяти, необходимой для хранения внутренних структур данных.

Ну, это даже не говорит, что внизу есть std::promise (аналогично для std::async), это может быть просто неопределенный тип, который можно подключить к std::future.

Поэтому, если действительно не указано, как std::packaged_task распределяет свое внутреннее общее состояние, лучше всего реализовать собственные средства для асинхронного вызова функций. Учитывая, что, говоря простым языком, std::packaged_task — это просто std::function в комплекте с std::promise, а std::async просто запускает std::packaged_task в новом потоке (ну, за исключением случаев, когда это не так), это не должно быть большой проблемой.

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

EDIT: Читая это снова, я думаю, что приведенная выше стандартная цитата действительно говорит о том, что общее состояние std::packaged_task выделяется с использованием предоставленного распределителя, поскольку оно является частью "внутренние структуры данных", что бы это ни было (хотя std::promise не обязательно). Поэтому я думаю, что std::packaged_task должно быть достаточно для явного управления общим состоянием std::future асинхронной задачи.

person Christian Rau    schedule 11.10.2012

Память выделяется потоком, который вызывает std::async, и вы не можете контролировать, как это делается. Обычно это будет делать какой-то вариант new __internal_state_type, но это не гарантируется; он может использовать malloc или специально выбранный для этой цели распределитель.

Из 30.6.8p3 [futures.async]:

«Эффекты: первая функция ведет себя так же, как вызов второй функции с аргументом политики launch::async | launch::deferred и теми же аргументами для F и Args. Вторая функция создает общее состояние, связанное с возвращаемым будущим объектом. ... "

«Первая функция» — это перегрузка без политики запуска, а вторая — перегрузка с политикой запуска.

В случае std::launch::deferred другого потока нет, поэтому все должно происходить в вызывающем потоке. В случае с std::launch::async 30.6.8p3 продолжает:

— если policy & launch::async не равно нулю — вызывает INVOKE (DECAY_COPY (std::forward<F>(f)), DECAY_COPY (std::forward<Args>(args))...) (20.8.2, 30.3.1.2) как будто в новом потоке выполнения, представленном объектом потока , при этом вызовы DECAY_COPY () оцениваются в потоке, вызвавшем async.< /сильный> ...

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

Конечно, вы можете написать реализацию, которая запускает новый поток, ждет, пока он выделит состояние, а затем возвращает future со ссылкой на него, но зачем вам это?

person Anthony Williams    schedule 11.10.2012
comment
Где гарантируется, что вызывающий поток выделяет хранилище? Хорошо, это вполне вероятно, но я все же хотел бы увидеть цитату из стандарта. - person Christian Rau; 11.10.2012
comment
Обновленный ответ с более подробной информацией. - person Anthony Williams; 11.10.2012
comment
Действительно, это кажется довольно ясным, я проглядел это. Но указание на это в вашем ответе, как вы это делаете сейчас, не может причинить никакого вреда. Спасибо и +1. - person Christian Rau; 11.10.2012
comment
@AnthonyWilliams Одна из причин, по которой пользователя может волновать, какой поток выделил хранилище, — это локальность. Если поток, созданный std::async, выполняется на ядре с более быстрым доступом к определенной области памяти, то имеет смысл локализовать асинхронное состояние в этой области. - person Jared Hoberock; 11.10.2012