Что происходит при переназначении будущего, которое еще не готово

Во время обзора кода я наткнулся на фрагмент кода, который в основном сводится к следующему:

#include <iostream>
#include <future>
#include <thread>

int main( int, char ** )
{
    std::atomic<int> x( 0 );
    std::future<void> task;
    for( std::size_t i = 0u; i < 5u; ++i )
    {
        task = std::async( std::launch::async, [&x, i](){
                std::this_thread::sleep_for( std::chrono::seconds( 2u * ( 5u - i ) ) );
                ++x;
            } );
    }

    task.get();
    std::cout << x << std::endl;
    return 0;
}

Я не был уверен,

  • гарантируется, что все задания выполняются при распечатке результата,
  • будут ли выполняться задачи один за другим (т.е. назначение задачи будет блокировать) или нет.

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

Теперь я узнал, что ответ на вопрос о том, что делает gcc-5, нерешительно, и это сделало меня еще более любопытным: можно было бы предположить, что назначение либо блокирует, либо не блокирует.

Если он блокируется, время, затрачиваемое программой, должно быть в основном суммой времени, которое отдельные задачи выполняют для выполнения. Первый занимает 10 секунд, второй 8, третий 6, четвертый 4 и последние 2 секунды. Таким образом, в общей сложности он должен принимать 10 + 8 + 6 + 4 + 2 = 30 секунд.

Если он не блокируется, он должен принимать до тех пор, пока последняя задача, т.е. 2 секунды.

Вот что происходит: требуется 18 секунд (измеряется с использованием времени. /a.out или старых добрых часов). Немного поработав с кодом, я обнаружил, что код ведет себя так, как будто назначение будет чередующимся образом блокировать и не блокировать.

Но это не может быть правдой, верно? std::async возможно, возвращается к std::deferred половине времени? Мой отладчик говорит, что он порождает два потока, блокирует до тех пор, пока оба потока не выйдут, а затем создаст еще два потока и т.д.

Что говорит стандарт? Что должно произойти? Что происходит внутри gcc-5?

Ответ 1

Назначение task через operator=(&&) не блокируется (см. ниже), но в вашем случае вы создали std::future с помощью std::async, поэтому он блокирует (благодаря @TC):

[future.async]

Если реализация выбирает политику запуска:: async,

  • [...]

  • связанное завершение потока синхронизируется с ([intro.multithread]) возвратом от первой функции, которая успешно обнаруживает состояние готовности общего состояния или с помощью возврата из последней функции, которая освобождает общий state, в зависимости от того, что произойдет раньше.

Почему вы получаете время выполнения 18 секунд?

Что происходит в вашем случае, так это то, что std::async запускает "поток" для вашего лямбда до назначения. См. ниже подробное объяснение того, как вы получаете время выполнения 18 секунд.

Это то, что (возможно) происходит в вашем коде (e означает epsilon):

  • t = 0, сначала std::async вызов с i = 0, начиная новый поток;
  • t = 0 + e, второй std::async вызов с i = 1, запускающий новый поток, затем переместите: перемещение освободит текущее общее состояние task, блокируя в течение примерно 10 секунд (но второе std::async с i = 1 уже выполняется);
  • t = 10, третий std::async вызов с i = 2, запускающий новый поток, а затем перемещение: текущим общим состоянием task был вызов с i = 1, который уже готов, поэтому ничего не блокируется;
  • t = 10 + e, четвертый std::async вызов с i = 3, запускающий новый поток, а затем перемещение: перемещение блокируется, потому что предыдущий std::async с i = 2 не готов, но поток для i = 3 как уже начало;
  • t = 16, пятый std::async вызов с i = 4, запускающий новый поток, затем переместите: текущее общее состояние task (i = 3) уже готово, поэтому неблокируется;
  • t = 16 + e, из циклов, вызовите .get() дождитесь готовности * общего состояния;
  • t = 18, общее состояние будет готово, так что весь материал заканчивается.

Стандартные сведения о std::future::operator=:

Вот стандартная цитата для operator= на std::future:

future& operator=(future&& rhs) noexcept;

Эффекты:

  • (10.1) - освобождает любое общее состояние (30.6.4).
  • ...

И вот что означает "Освобождение любого общего состояния" (акцент мой):

Когда предполагается, что асинхронный объект возврата или асинхронный поставщик освобождают его общее состояние, это означает:

(5.1) - [...]

(5.2) - [...]

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

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

Ответ 2

гарантируется выполнение всех задач при печати результата,

Гарантируется выполнение только назначенной последней задачи. По крайней мере, я не мог найти никаких правил, которые гарантировали бы остальные.

будут ли выполняться задачи один за другим (т.е. назначение задачи будет блокировать) или нет.

Назначение задачи обычно не блокируется, но в этом случае оно может блокироваться - без гарантии.

[futures.unique_future]

Будущее & operator = (future & rhs) noexcept;

  1. Действие:

    освобождает общее состояние ([futures.state]).

[futures.state]

  1. Когда предполагается, что асинхронный объект возврата или асинхронный поставщик освобождают его общее состояние, это означает:

    • если возвращаемый объект или поставщик содержат последнюю ссылку на его общее состояние, общее состояние уничтожается; и

    • возвращаемый объект или поставщик отказывается от ссылки на его общее состояние; и

    • эти действия не будут блокировать для состояния общего состояния, чтобы быть готовым, за исключением того, что он может блокировать, если все из них истинны: общее состояние было создано вызовом std:: async, общее состояние еще не готово, и это была последняя ссылка на общее состояние.

Все условия потенциальной блокировки верны для задачи, созданной std::async, еще не выполненной.