Repository navigation
Build error when starting exec::task in some schedulers, because they miss the scheduler_concept typedef #2134
Description
Hi,
we recently noticed that you cannot build a program where an exec::task is started in some schedulers. In particular exec::static_thread_pool and exec::timed_thread_context are affecting us.
This was the most minimal example that i could find:
#include <exec/static_thread_pool.hpp>
#include <exec/task.hpp>
#include <stdexec/execution.hpp>
auto main() -> int
{
exec::static_thread_pool ctx;
stdexec::sync_wait(stdexec::starts_on(ctx.get_scheduler(), [&]()-> exec::task<void>{
co_return;
}()));
return 0;
}Compile error
[main] Building folder: /home/coder/stdexec/build/base-debug example.hello_world
[build] Starting build
[proc] Executing command: /usr/bin/cmake --build /home/coder/stdexec/build/base-debug --target example.hello_world --
[build] [0/2] Re-checking globbed directories...
[build] [1/3] Building CXX object examples/CMakeFiles/example.hello_world.dir/hello_world.cpp.o
[build] FAILED: examples/CMakeFiles/example.hello_world.dir/hello_world.cpp.o
[build] /usr/bin/g++ -DTBB_USE_DEBUG -isystem /home/coder/stdexec/include -isystem /home/coder/stdexec/build/base-debug/include -g -std=gnu++20 -Wno-maybe-uninitialized -fcoroutines -fconcepts-diagnostics-depth=10 -Wall -Werror=unused-parameter -Wno-non-template-friend -MD -MT examples/CMakeFiles/example.hello_world.dir/hello_world.cpp.o -MF examples/CMakeFiles/example.hello_world.dir/hello_world.cpp.o.d -o examples/CMakeFiles/example.hello_world.dir/hello_world.cpp.o -c /home/coder/stdexec/examples/hello_world.cpp
[build] In file included from /home/coder/stdexec/examples/hello_world.cpp:19:
[build] /home/coder/stdexec/include/exec/task.hpp: In instantiation of ‘constexpr experimental::execution::__task::__default_task_context_impl<_SchedulerAffinity>::__default_task_context_impl(_ParentPromise&) [with _ParentPromise = stdexec::__connect_await::__promise<experimental::execution::__task::basic_task<void, experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky> >, stdexec::__seq::__rcvr2<stdexec::__sync_wait::__receiver<>, stdexec::__sched_env<experimental::execution::_pool_::_static_thread_pool::scheduler, stdexec::__env::__fwd<const stdexec::__sync_wait::__env&> > > >; experimental::execution::__task::__scheduler_affinity _SchedulerAffinity = experimental::execution::__task::__scheduler_affinity::__sticky]’:
[build] /home/coder/stdexec/include/stdexec/__detail/__manual_lifetime.hpp:52:28: required from ‘constexpr _Ty& stdexec::__manual_lifetime<_Ty>::__construct(_Args&& ...) [with _Args = {stdexec::__connect_await::__promise<experimental::execution::__task::basic_task<void, experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky> >, stdexec::__seq::__rcvr2<stdexec::__sync_wait::__receiver<>, stdexec::__sched_env<experimental::execution::_pool_::_static_thread_pool::scheduler, stdexec::__env::__fwd<const stdexec::__sync_wait::__env&> > > >&}; _Ty = experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky>]’
[build] 52 | return *std::launder(::new (static_cast<void*>(__buffer_))
[build] | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[build] 53 | _Ty{static_cast<_Args&&>(__args)...});
[build] | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[build] /home/coder/stdexec/include/stdexec/__detail/__optional.hpp:110:41: required from ‘constexpr _Tp& stdexec::__opt::__optional<_Tp>::emplace(_Us&& ...) [with _Us = {stdexec::__connect_await::__promise<experimental::execution::__task::basic_task<void, experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky> >, stdexec::__seq::__rcvr2<stdexec::__sync_wait::__receiver<>, stdexec::__sched_env<experimental::execution::_pool_::_static_thread_pool::scheduler, stdexec::__env::__fwd<const stdexec::__sync_wait::__env&> > > >&}; _Tp = experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky>]’
[build] 110 | _Tp& __t = __value_.__construct(static_cast<_Us&&>(__us)...);
[build] | ~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[build] /home/coder/stdexec/include/exec/task.hpp:676:47: required from ‘constexpr std::__n4861::coroutine_handle<void> experimental::execution::__task::basic_task<_Ty, _Context>::__task_awaiter<_ParentPromise>::await_suspend(std::__n4861::coroutine_handle<_ParentPromise2>) [with _ParentPromise2 = stdexec::__connect_await::__promise<experimental::execution::__task::basic_task<void, experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky> >, stdexec::__seq::__rcvr2<stdexec::__sync_wait::__receiver<>, stdexec::__sched_env<experimental::execution::_pool_::_static_thread_pool::scheduler, stdexec::__env::__fwd<const stdexec::__sync_wait::__env&> > > >; _ParentPromise = stdexec::__connect_await::__promise<experimental::execution::__task::basic_task<void, experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky> >, stdexec::__seq::__rcvr2<stdexec::__sync_wait::__receiver<>, stdexec::__sched_env<experimental::execution::_pool_::_static_thread_pool::scheduler, stdexec::__env::__fwd<const stdexec::__sync_wait::__env&> > > >; _Ty = void; _Context = experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky>]’
[build] 676 | __coro_.promise().__context_.emplace(__parent.promise());
[build] | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~
[build] /home/coder/stdexec/include/stdexec/__detail/__connect_awaitable.hpp:136:41: required from ‘constexpr auto stdexec::__connect_await::__awaitable_wrapper<_Derived>::await_suspend(std::__n4861::coroutine_handle<_Promise>) [with _Promise = stdexec::__connect_await::__promise<experimental::execution::__task::basic_task<void, experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky> >, stdexec::__seq::__rcvr2<stdexec::__sync_wait::__receiver<>, stdexec::__sched_env<experimental::execution::_pool_::_static_thread_pool::scheduler, stdexec::__env::__fwd<const stdexec::__sync_wait::__env&> > > >; _Derived = stdexec::__connect_await::__awaitable_state<experimental::execution::__task::basic_task<void, experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky> >, stdexec::__connect_await::__promise<experimental::execution::__task::basic_task<void, experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky> >, stdexec::__seq::__rcvr2<stdexec::__sync_wait::__receiver<>, stdexec::__sched_env<experimental::execution::_pool_::_static_thread_pool::scheduler, stdexec::__env::__fwd<const stdexec::__sync_wait::__env&> > > > >::__state]’
[build] 136 | return __awaiter().await_suspend(__h);
[build] | ~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~
[build] /home/coder/stdexec/include/stdexec/__detail/__connect_awaitable.hpp:134:52: required from ‘constexpr auto stdexec::__connect_await::__awaitable_wrapper<_Derived>::await_suspend(std::__n4861::coroutine_handle<_Promise>) [with _Promise = stdexec::__connect_await::__promise<experimental::execution::__task::basic_task<void, experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky> >, stdexec::__seq::__rcvr2<stdexec::__sync_wait::__receiver<>, stdexec::__sched_env<experimental::execution::_pool_::_static_thread_pool::scheduler, stdexec::__env::__fwd<const stdexec::__sync_wait::__env&> > > >; _Derived = stdexec::__connect_await::__awaitable_state<experimental::execution::__task::basic_task<void, experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky> >, stdexec::__connect_await::__promise<experimental::execution::__task::basic_task<void, experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky> >, stdexec::__seq::__rcvr2<stdexec::__sync_wait::__receiver<>, stdexec::__sched_env<experimental::execution::_pool_::_static_thread_pool::scheduler, stdexec::__env::__fwd<const stdexec::__sync_wait::__env&> > > > >]’
[build] 134 | noexcept(noexcept(__awaiter().await_suspend(__h)))
[build] | ~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~
[build] /home/coder/stdexec/include/stdexec/__detail/__connect_awaitable.hpp:408:73: required from ‘void stdexec::__connect_await::__opstate<_Awaiter, _Receiver>::start() & [with _Awaitable = experimental::execution::__task::basic_task<void, experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky> >; _Receiver = stdexec::__seq::__rcvr2<stdexec::__sync_wait::__receiver<>, stdexec::__sched_env<experimental::execution::_pool_::_static_thread_pool::scheduler, stdexec::__env::__fwd<const stdexec::__sync_wait::__env&> > >]’
[build] 408 | using __suspend_result_t = decltype(__awaiter_.await_suspend(__coro));
[build] | ~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
[build] /home/coder/stdexec/include/stdexec/__detail/__operation_states.hpp:133:17: required from ‘constexpr void stdexec::start_t::operator()(_Op&) const [with _Op = stdexec::__connect_await::__opstate<experimental::execution::__task::basic_task<void, experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky> >, stdexec::__seq::__rcvr2<stdexec::__sync_wait::__receiver<>, stdexec::__sched_env<experimental::execution::_pool_::_static_thread_pool::scheduler, stdexec::__env::__fwd<const stdexec::__sync_wait::__env&> > > >]’
[build] 133 | __op.start();
[build] | ~~~~~~~~~~^~
[build] /home/coder/stdexec/include/stdexec/__detail/__sequence.hpp:290:25: required from ‘constexpr void stdexec::__seq::__opstate<_CvSender1, _Sender2, _Receiver>::__start_next() [with _CvSender1 = stdexec::{anonymous}::__sexpr<<lambda closure object>stdexec::<lambda()>()>; _Sender2 = experimental::execution::__task::basic_task<void, experimental::execution::__task::__default_task_context_impl<experimental::execution::__task::__scheduler_affinity::__sticky> >; _Receiver = stdexec::__sync_wait::__receiver<>]’
[build] 290 | STDEXEC::start(__op);
[build] | ^
[build] /home/coder/stdexec/include/stdexec/__detail/__sequence.hpp:284:22: required from here
[build] 284 | constexpr void __start_next() noexcept
[build] | ^~~~~~~~~~~~
[build] /home/coder/stdexec/include/exec/task.hpp:163:33: error: no match for ‘operator=’ (operand types are ‘experimental::execution::__task::__any_scheduler’ and ‘experimental::execution::_pool_::_static_thread_pool::scheduler’)
[build] 163 | this->__scheduler_ = __parent_sched;
[build] | ~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~
[build] /home/coder/stdexec/include/exec/task.hpp:163:33: note: there are 2 candidates
[build] /home/coder/stdexec/include/exec/task.hpp:64:12: note: candidate 1: ‘constexpr experimental::execution::__task::__any_scheduler& experimental::execution::__task::__any_scheduler::operator=(const experimental::execution::__task::__any_scheduler&)’
[build] 64 | struct __any_scheduler
[build] | ^~~~~~~~~~~~~~~
[build] /home/coder/stdexec/include/exec/task.hpp:64:12: note: no known conversion for argument 1 from ‘experimental::execution::_pool_::_static_thread_pool::scheduler’ to ‘const experimental::execution::__task::__any_scheduler&’
[build] /home/coder/stdexec/include/exec/task.hpp:64:12: note: candidate 2: ‘constexpr experimental::execution::__task::__any_scheduler& experimental::execution::__task::__any_scheduler::operator=(experimental::execution::__task::__any_scheduler&&)’
[build] /home/coder/stdexec/include/exec/task.hpp:64:12: note: no known conversion for argument 1 from ‘experimental::execution::_pool_::_static_thread_pool::scheduler’ to ‘experimental::execution::__task::__any_scheduler&&’
[build] ninja: build stopped: subcommand failed.
[proc] The command: /usr/bin/cmake --build /home/coder/stdexec/build/base-debug --target example.hello_world -- exited with code: 1
[driver] Build completed: 00:00:02.833
[build] Build finished with exit code 1
With the scheduler from exec::timed_thread_context the same problem occurs. For exec::single_thread_context, which returns the scheduler for stdexec::run_loop, it compiles.
I guess it comes from the __semi_scheduler concept not being fulfilled:
Lines 55 to 62 in 711da59
| // A scheduler concept that does not check for copyability since that creates a cycle | |
| // in the type system. | |
| template <class _Scheduler> | |
| concept __semi_scheduler = requires(_Scheduler& __sched) { | |
| typename _Scheduler::scheduler_concept; | |
| requires __std::derived_from<typename _Scheduler::scheduler_concept, scheduler_tag>; | |
| { schedule(__sched) } -> sender; | |
| }; |
The affected schedulers do not have this scheduler_concept typedef - hence the concept is not fulfilled. When the scheduler_concept typedef is added, the build error is also fixed.
Have these particular scheduler implementations just not been updated or is it a problem of the task-implementation that those schedulers cannot be used?
Thanks in advance for any answers.
Activity
ashat1701 commentedon Jul 28, 2026
I have same error, I think it introduced in one of the latest versions. When I added
using scheduler_concept = STDEXEC::scheduler_there:stdexec/include/exec/thread_pool_base.hpp
Lines 111 to 115 in 711da59
all my tests pass.
scheduler_conceptentity were added in this PR - #2038, and I think we should add tests for all schedulers in exec module in order to avoid these errorsmathisloge commentedon Aug 28, 2026
Heads-up: adding
using scheduler_concept = STDEXEC::scheduler_t;tothread_pool_base::schedulerfixes the build, but it is not a safe fix. Now the task silently runtime hangs.I hit this with
exec::taskrunning onexec::asio::asio_thread_pool(also derives fromexec::thread_pool_base). With the typedef added, the awaited sender chain runs to completion, but theexec::taskcoroutine is never resumed. Every thread is idle and the task is simply abandoned. In my case that meant every gRPC handler stopped sending its response.Isolating the typedef as the only variable:
scheduler_concepttypedef5473e9da(just before #2038)5473e9daf91f636(main)f91f636same stdexec commit, typedef as the only change, working → hung.
git bisectover my test suite gives b4a598d (#2038) as the first bad commit. My read is that the typedef makes the scheduler claim to satisfystdexec::scheduler, which switchesexec::taskinto enforcing affinity onto a scheduler that doesn't fully implement the protocol, so completions are dropped.So
thread_pool_base::schedulerprobably needs to actually satisfy the concept rather than just declare it .Caveat: the hang reproduces reliably in my application but I haven't managed to reduce it to a small standalone repro yet
ashat1701 commentedon Sep 1, 2026
Hi, @mathisloge, did you consider using
stdexec::task_schedulertype erasure. For my build (i build everyting for wasm, so it can't represent even most common way) it fixes all problems and my event loop don't hang. I just curious what is main reason why can completion dropalwaysprince05 commentedon Sep 14, 2026
Thanks for the great write-up — the diagnosis was spot on.
You were right that
__semi_scheduleris the culprit: it required ascheduler_concepttypedef, whichstatic_thread_poolandtimed_thread_contextsimply don't define (and don't need to, since thepublic
schedulerconcept is satisfied without it).I've opened #2256, which drops that requirement from the concept and adds
regression tests that co_start a task on both affected schedulers. With the
fix, your minimal example compiles and runs as expected. Feedback welcome!
fix(exec): add scheduler_concept typedef to all stdexec schedulers (N…
fix: provide the scheduler_concept nested alias on all stdexec schedu…
fix: provide the scheduler_concept nested alias on all stdexec schedu…
test: add scheduler_concept regression guard for all stdexec schedule…