Разрушение информации о типе результата std::packaged_task

Sir-VestniK
Sir-VestniK

На данный момент тип врзвращаемого значения функции лежащей в packaged_task присутствует в шаблонных параметрах данного класса. Из за чего два packaged_task с одинаковыми типами принимаемых параметров несовместимы друг с другом, если функторы в них лежащие возвращают разные типы. Это требует написание дополнительных type-erasure классов над packaged_task при реализации очередей задач и пулов потоков.

Предлагаю завести частичную специализацию std::packaged_task для типа возвращаемого значения

namespace std {
using ignore_t = decltype(ignore);
}

  которая оличается от общего случая следующими моментами:

  1. Доступен только конструктор по умолчанию создающий невалидную задачу, конструктор перемещения и конструктор от r-value задачи с произвольным типом результата
    template<typename A...>
    template<typename R>
    std::packaged_task<std::ignore_t(A...)>::packaged_task(std::packaged_task<R(A...)>&&);​

    при этом данный конструктор бросает исключение типа std::future_error с кодом std::future_errc::broken_promise если от перемещаемой задачи не вызывался метод get_future.

  2. Данная специализация не предоставляет метода get_future.

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

Предлагаю данную специализацию продвигать как дополнение к Concurrency TS без которого std::future и ассоциированные с ним классы всё равно не пригодны для реального использования в боевом коде.

4
рейтинг
14 комментариев
yndx-antoshkka
Распишите пожалуйста подробнее, какого эффекта вы желаете добиться? Если вам нужно просто хранить функции/функторы, то кажется что std::function<void(A...)> вам должен подойти. Если вам нужен future, а сам результат не нужен - то возможно std::packaged_task<void(A...)> уже делает всё необходимое.
yndx-antoshkka
Sir-VestniK
yndx-antoshkka, std::funcition может хранить только CopyConstructable и CopyAssignable функциональные объекты, так что он не подходит под MoveConstructable, MoveAssignable тип std::packaged_task.

std::packaged_task<void(A...)> аллоцирует дополнительный shared state и синхронизует доступ к нему при вызове operator() в то время как в реальности этого не требуется.

Пример того что хочется иметь возможность делать:

* Допустим у нас есть вот такая очередь или ей подобная: github.com/VestniK/portable_concurrency/blob/master/test/closable_queue.h
* есть пул соединений с базой, пул потоков висящий на этой очереди и глобальный std::atomic<bool> сигнализирующий о необходимости завершить приложение.
* хочется без лишних самописных type-erasure, доп аллокаций и ненужной синхронизации на дополнительном shared state писать подобные функции:

template<typename F>
auto enque_task(closable_queue<std::packaged_task<std::ignore_t(std::atomic<bool>&, sql::connection&)>>& queue, F&& f) {
using R = std::result_of_t<F(std::atomic<bool>&, sql::connection&)>;
std::packaged_task<R> task{std::forward<F>(f)};
auto res = task.get_future();
queue.emplace(std::move(task));
return res;
}

наружу ушёл std::future знающий тип результата, а в функции разгребающие очередь ушли упакованные задачи. Сделть подобное только через интерфейс std::packaged_task эффективно невозможно, так как shared state который он оборачивает невидим никоим образом через API стандартной библиотеки.
Sir-VestniK
Sir-VestniK
Sir-VestniK, перформатирую фрагмент кода, так как он абсолютно нечитабелен

using task_signature = std::ignore_t(std::atomic<bool>&, sql::connection&);

template<typename F>
auto enque_task(
closable_queue<std::packaged_task<task_signature>>& queue,
F&& f
) {
using R = std::result_of_t<F(std::atomic<bool>&, sql::connection&)>;
std::packaged_task<R> task{std::forward<F>(f)};
auto res = task.get_future();
queue.emplace(std::move(task));
return res;
}
Sir-VestniK
yndx-antoshkka
Если я вас верно понял, то вашу проблему более-менее можно решить через std::promise:

using task_signature = void(std::atomic<bool>&, sql::connection&);

template<typename F>
auto enque_task(closable_queue<std::function<task_signature>>& queue, F&& f) {
using R = std::result_of_t<F(std::atomic<bool>&, sql::connection&)>;
std::promise<R> p;
auto res = p.get_future();
queue.emplace(
[f = std::forward<F>(f), p = std::move(p)](std::atomic<bool>& b, sql::connection& c) {
p.set_value(f(b, c));
}
);
return res;
}

Но останется недостаток в виде возможной динамической аллокации внутри std::function. Или F у вас ещё и не копируемый? Или вы ещё и от shared_state хотите избавиться?
yndx-antoshkka
Sir-VestniK
yndx-antoshkka, как и packaged_task, promise является MoveOnly типом и лямдба в которую мы захватили его через move тоже лишилась конструктора копирования.

Все типы являющиеся пишущими владельцами shared-state некопируемы. Исключением может являться shared_future полученный через std::async(std::launch::deferred,...).share() его можно скопировать в разные потоки и тот кто первый скажет get позовёт захваченный функтор, а все следующие будут дожитаться его завершения и только получат результат. Но с таким подходом предсказать поток в котором позовётся функция крайне сложно и ненароком можно надолго занять IO либо GUI поток, а этого делать как правило никогда не хочется.

Функтор отдаваемый на асинхронное исполнение тоже часто бывает некопируемым. Я отдал критичный ресурс на владение задаче которая с ним работает и передаст его мне обратно либо разрушит по завершению, разумеется ресурс будет некопируемым и сам функтор тоже.

В результате пишется либо вышивание крестиком в виде: сложим promise либо packaged_task в shared_ptr, его захватим в лямбду, которую сложим в std::function а внутри лямбды позовём. Либо пишется универсальный type_erasure для MoveOnly функторов который гарантирует одноразовый вызов функции... стоп, но это же packaged_task, только с перламутровыми пуговицами в виде отсутствия знания о возвращаемом результате.

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

Чего хочется: специализации packaged_task которая больше не знает о типе вовращаемого значения и просто через move конструктор от packaged_task с произвольным типом результата функтора забирает себе во владение пишущую ссылку на shared-state задачи. После чего экземпляр такого packaed_task может быть отдан исполнителю которому важна только сигнатура задачи.

Уже в двух последних проектах на околосамописных future по мотивам std::future с данной проблемой сталкивался. Хочется, чтобы вместе с future::then в стандарт (насколько я понимаю уже не раньше чем в C++20) пришёл и метод решения данной проблемы.
Sir-VestniK
yndx-antoshkka
Ага. То есть вам нужно что-то наподобие

template <class... Args>
class packaged_task_base<Args...> {
void operator()(Args... args);
};


template <class R, class... Args>
class packaged_task<R(Args...)>: public packaged_task_base<Args...> {
future<R> get_furute();
};

Мне идея очень нравится! Вы готовы заняться написанием proposal? По идее он будет достаточно небольшой.
yndx-antoshkka
Sir-VestniK
yndx-antoshkka, Да, на выходных начну писать документ в своей реализации Concurrency TS на гитхабе, там же сделаю прототип.
Sir-VestniK
Sir-VestniK
yndx-antoshkka, начал работу над proposal здесь: github.com/VestniK/portable_concurrency/blob/result-erased-task-proposal/proposal/proposal.tex когда законченый черновой вариант будет готов отпишусь комментарием в данном обсуждении ещё раз.
Sir-VestniK
Sir-VestniK
yndx-antoshkka, Написал текст предложения. LaTeX исходник по ссылке выше, PDF версия: yadi.sk/i/Mn7hi_ax3EFmkm
Sir-VestniK
yndx-antoshkka
Выглядит хорошо. Пара комментариев:
* Надо добавить свой email для обратной связи в proposal.
* Есть пара опечаток (в том числе в заголовках)
* Опишите на английском вашу идею в рассылке std-proposals@isocpp.org (тема письма должна начинаться с [std-proposals]) и приглядывайтесь к коментариям
yndx-antoshkka
yndx-antoshkka
Sir-VestniK, помимо выше озвученного, надо еще добыть официальный номер для своего proposal. Как будете готовы - пишите, помогу.
yndx-antoshkka
Sir-VestniK
yndx-antoshkka, Извиняюсь, что пропал. Было много семейных хлопот связанных с рождением первого ребёнка в семье :)

По результатам обсуждения на std-proposals groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/doVBK_QOsyY пришли к выводу, что данное предложение решает задачу которая может быть решена посредством std::unique_function (N4543). При этом unique_function является более общим решением и лучше ориентироваться на него.
Sir-VestniK
yndx-antoshkka
Sir-VestniK, мои поздравления!

Ну а по поводу proposal - если у вас есть какие-то пожелания по N4543, пишите их сюда. Я передам остальным людям в комитете.
yndx-antoshkka
Sir-VestniK
yndx-antoshkka, я хочу написать в своём репозитории в отдельной ветке реализацию unique_function с гарантией отсутствия двойного разрушения информации о типе при работе с packaged_task. Такая гарантия уже даётся при работе с указателями на функции и std::function в самом N4543. Добавив такое трбование и на packaged_task можно добиться, чтобы unique_function максимально эффективно работал со всеми обёртками над функциональными объектами предоставляемыми стандартной библиотекой.

Реализацией займусь месяца через полтора. Сначала хочу добить when_any и when_all, чтобы иметь минимальную реализацию ConcurrencyTS на которой можно будет писать proof-of-concept'ы.
Sir-VestniK
Другие идеи
Группа создана, чтобы собирать предложения к стандарту C++, организовывать их внутренние обсуждения, помогать готовить их для отправки в комитет и защищать на общих собраниях в рабочей группе по С++ Международной организации по стандартизации (ISO).