expected<T, E>-like asynchronous tasks with .on_finish() support and ability to adapt third-party tasks API.
#include <rename_me/future_task.h>
#include <chrono>
#include <cstdio>
#include <cassert>
int main()
{
auto do_work = [](nn::Scheduler& scheduler, int input) -> nn::Task<int>
{
auto task = make_task(scheduler
// Run std::async() and wrap std::future<> into Task<>
, std::async(std::launch::async, [=]()
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return (2 * input);
}))
// When std::async() completes
.then([](const nn::Task<int, std::exception_ptr>& task)
{
// There was no exception
assert(task.is_successful());
return (3 * task.get().value());
});
// When our `task` completes. Ignore any values, just print something
(void)task.then([]
{
std::puts("Completed");
});
return task;
};
nn::Scheduler scheduler;
nn::Task<int> task = do_work(scheduler, 10);
while (task.is_in_progress())
{
scheduler.poll();
}
return task.get().value(); // Returns 10 * 2 * 3 = 60
}
See simple_then example.
-
Add support of custom allocators. One allocator for scheduler ? Or one allocator per task ? (allocator per task will be hard to add/implement with existing interface).
-
How Scheduler can be customized ?
-
Unify nn::expected<> API to be consistent.
-
Add supporting API (like,
when_all(tasks...).on_finish(...)
). -
Tests: ensure thread-safe stuff.
-
Polish memory layout for internal tasks.
-
Default (thread-local ?) Scheduler's ?
-
Compare to:
- continuable (https://github.com/Naios/continuable)
- asynqro (https://github.com/dkormalev/asynqro)
- Threading Building Blocks (https://www.threadingbuildingblocks.org/)
- transwarp (https://github.com/bloomen/transwarp)
- Parallel Patterns Library (https://docs.microsoft.com/en-us/cpp/parallel/concrt/parallel-patterns-library-ppl?view=vs-2017)
- https://vittorioromeo.info/index/blog/zeroalloc_continuations_p0.html
- hpx (https://github.com/STEllAR-GROUP/hpx)
- stlab (https://github.com/stlab/libraries)
-
[can't be done without some kind of RTTI] Attach
Info
to the task. This allows to have:- NoInfo (in Retail, for example)
- configurable with NN_DISABLE_INFO
- DebugInfo (were started and so on)
- ExecutionInfo (parent/child relationships, see transwrap library)
- configurable with NN_ENABLE_TASKS_GRAPH
- CustomInfo - allows to attach custom information by the client
- NoInfo (in Retail, for example)
struct NoInfo {};
struct DebugInfo : NoInfo { const char* const file, unsigned line; /**/ };
struct ExecutionInfo : DebugInfo { TaskHandle parent; /**/ };
#if (NN_DISABLE_INFO)
using TaskInfo = NoInfo;
#else
#if (NN_ENABLE_TASKS_GRAPH)
using TaskInfo = ExecutionInfo;
#else
using TaskInfo = DebugInfo;
#endif
#endif
struct CustomInfo : TaskInfo { int cookie; /**/ };
-
Because of Cancel, we need to require Error to be default-constructible to be able to create expected<..., Error> in any case, when cancel() happens in runtime.
This mostly needed to ensure that no matter what - client can always get expected<...> from the Task<...> even if it was canceled. It's custom's task responsibility to handle cancel nicely. This also allows to make nice API that unwraps Task<T, E> to
on_success([](T&&) {})
andon_fail([](E&&, bool canceled))
, for example.Looks like this allows also remove LazyStorage<> since we always can build default expected with error state.
-
Make generalization: let client provides customization of our traits that will tell how to get/set success and error values to some type T. We can do something like
template<typename Data> BasicTask
and Task beusing template<typename T, typename E> Task = BasicTask<expected<T, E>>
.This allows to optimize storage even more for cases like
expected<void, void>
.
template<typename Data>
struct DataTraits;
template<typename T, typename E>
struct DataTraits<expected<T, E>>
{
using value_type = T;
using error_type = E;
using type = expected<T, E>;
value_type& get_value(expected<T, E>&) {}
error_type& get_error(expected<T, E>&) {}
type build_value(...) {}
type build_error(...) {}
};
// Special case for DataTraits<void>
- When (13.) will be done, on_fail() and on_cancel() and on_success() API can accept raw value among whole task:
Task<expected<int, char>> task = ...;
task.on_success([](int data) {});
task.on_fail([](char data) {});
task.on_fail([](char data, bool canceled) {});
task.on_fail([](const Task<expected<int, char>>& task) {});
task.on_canceled([](char data) {});
TODO: some C++ 17 stuff is used, can be back-ported to C++ 11
mkdir build && cd build
cmake -G "Visual Studio 15 2017 Win64" ..
# cmake ..
cmake --build . --config Release
Clang integration into MSVC 2017 on latest Clang (8.0).
- go to and install LLVM with "LLVM Compiler Toolchain Visual Studio extension"
- run CMake with "-T LLVM" toolset (case sensetive)