Как std::future влияет на время жизни связанной std::packaged_task?

У меня есть std::packaged_task, содержащий лямбду, которая копирует переменную. Когда этот std::packaged_task будет удален, я ожидаю, что переменная, живущая внутри лямбды, будет уничтожена, но я заметил, что если я получаю связанный std::future для этого std::packaged_task, объект future продлевает время жизни переменной внутри лямбды.

Например:

#include <iostream>
#include <future>

class Dummy
{
public:
    Dummy() {std::cout << this << ": default constructed;" << std::endl;}
    Dummy(const Dummy&) {std::cout << this << ": copy constructed;" << std::endl;}
    Dummy(Dummy&&) {std::cout << this << ": move constructed;" << std::endl;}
    ~Dummy() {std::cout << this << ": destructed;" << std::endl;}
};

int main()
{
    std::packaged_task<void()>* p_task;
    {
        Dummy ScopedDummy;
        p_task = new std::packaged_task<void()>([ScopedDummy](){std::cout << "lambda call with: " << &ScopedDummy << std::endl;});
        std::cout << "p_task completed" << std::endl;
    }
    {
        std::future<void> future_result;
        {
            future_result = p_task->get_future();
            (*p_task)();
            delete p_task;
        }
        std::cout << "after p_task has been deleted, the scope of future_result determines the scope of the dummy inside p_task" << std::endl;
    }
    std::cout << "p_task cleans up when future_result dies" << std::endl;
}

Возможный вывод:

0x7fff9cf873fe: default constructed;
0x7fff9cf873ff: copy constructed;
0x1904b38: move constructed;
0x7fff9cf873ff: destructed;
0x7fff9cf873fe: destructed;
lambda call with: 0x1904b38
after p_task has been deleted, the scope of future_result determines the scope of the dummy inside p_task
0x1904b38: destructed;
p_task cleans up when future_result dies

Таким образом, время жизни объекта внутри лямбды увеличивается на область действия future_result.

Если мы закомментируем строку future_result = p_task->get_future();, возможный вывод:

0x7fff57087896: default constructed;
0x7fff57087897: copy constructed;
0x197cb38: move constructed;
0x7fff57087897: destructed;
0x7fff57087896: destructed;
lambda call with: 0x197cb38
0x197cb38: destructed;
after p_task has been deleted, the scope of future_result determines the scope of the dummy inside p_task
p_task cleans up when future_result dies

Мне было интересно, какой механизм здесь задействован, содержит ли std::future какую-то ссылку, которая поддерживает связанные объекты?


person Flavio Pingas    schedule 09.11.2017    source источник
comment
какой компилятор вы используете? clang выдает ожидаемый результат, то есть задача уничтожается, когда упакованная задача... похоже, ваш компилятор рассматривает задачу как часть общего состояния, мне это не кажется правильным...   -  person Massimiliano Janes    schedule 09.11.2017
comment
Я использую gcc 7.2.0. Вы правы, clang действительно производит второй вывод. Как вы думаете, это будет считаться ошибкой?   -  person Flavio Pingas    schedule 09.11.2017
comment
хорошо, исходники gcc libstdc++ показывают что задача действительно хранится в общем состоянии. Мне это кажется неправильным, хотя стандарт явно не запрещает хранить копию в общем состоянии... не знаю, посмотрим, что скажут другие...   -  person Massimiliano Janes    schedule 09.11.2017
comment
переосмыслив это, поведение gcc оказывается законным (хотя стандарт, кажется, не полностью соответствует этому), я опубликую ответ позже...   -  person Massimiliano Janes    schedule 09.11.2017


Ответы (1)


просматривая исходники packaged_task gcc7.2.0 , мы читаем:

packaged_task(allocator_arg_t, const _Alloc &__a, _Fn &&__fn)
    : _M_state(__create_task_state<_Res(_ArgTypes...)>(std::forward<_Fn>(__fn), __a)){}

~packaged_task()
{
  if (static_cast<bool>(_M_state) && !_M_state.unique())
    _M_state->_M_break_promise(std::move(_M_state->_M_result));
}

где _M_state — это shared_ptr для внутреннего общего состояния packaged_task. Итак, получается, что gcc хранит callable как часть packaged_task shared state, тем самым привязывая время жизни callable к тому, кто из packaged_task,future,shared_future умирает последним.

для сравнения, clang не уничтожает вызываемый объект при уничтожении упакованной задачи (фактически, моя копия clang будет хранить вызываемый объект как правильный член).

Кто прав? в стандарте не очень четко указано время жизни хранимой задачи; с одной стороны имеем

[[futures.task]]

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

packaged_task(F&& f)[...]Создает новый объект packaged_task с общим состоянием и инициализирует сохраненную задачу объекта с помощью std::forward(f).

packaged_task(packaged_task&& rhs)[...]Перемещает сохраненную задачу из rhs в *this.

reset()[...]Эффекты: как если бы *this = packaged_task(std::move(f)), где f — задача, хранящаяся в *this.

это предполагает, что callable принадлежит packaged_task, но у нас также есть

[[futures.state]]

- Многие из представленных в этом подпункте классов используют некоторое состояние для сообщения результатов. Это общее состояние состоит из некоторой информации о состоянии и некоторого результата (возможно, еще не оцененного), который может быть значением (возможно, недействительным) или исключением. [Примечание: фьючерсы, обещания и задачи, определенные в этом пункте, ссылаются на такое общее состояние. — примечание]

-[Примечание: результатом может быть объект любого типа, включая функцию для вычисления этого результата, как в случае асинхронного [...]]

а также

[futures.task.members]

-packaged_task(F&& f);[...]Вызов копии f должен вести себя так же, как вызов f[...] -~packaged_task(); Эффекты: отказывается от любого общего состояния

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

В целом, я думаю, что clang следует формулировке более последовательно, хотя ничто не кажется явно запрещающим поведение gcc. Тем не менее, я согласен, что это должно быть лучше задокументировано, потому что в противном случае это может привести к неожиданным ошибкам...

person Massimiliano Janes    schedule 10.11.2017
comment
+1 В ответ на сообщение об ошибке приложения, которое я получил, я написал небольшую тестовую программу, которая подтверждает это поведение для std::async. Это становится еще хуже, когда создается shared_future, потому что тогда задача не будет уничтожена на get(), а только на уничтожении последнего shared_future — что и ожидается из того, что вы описали. (Теперь я заменил оскорбительный код использованием пула потоков отслеживания зависимостей, написанным вашим покорным слугой, который уничтожает задачу сразу после ее завершения.) - person Arne Vogel; 12.06.2018