Skip to content

Build error when starting exec::task in some schedulers, because they miss the scheduler_concept typedef #2134

Closed

Description

@miwechner

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:

// 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

  1. ashat1701 commented on Jul 28, 2026

    @ashat1701

    I have same error, I think it introduced in one of the latest versions. When I added using scheduler_concept = STDEXEC::scheduler_t here:

    struct scheduler
    {
    private:
    template <class DerivedPoolType_, class Receiver>
    friend struct _pool_::opstate;

    all my tests pass. scheduler_concept entity were added in this PR - #2038, and I think we should add tests for all schedulers in exec module in order to avoid these errors

  2. mathisloge commented on Aug 28, 2026

    @mathisloge
    Contributor

    Heads-up: adding using scheduler_concept = STDEXEC::scheduler_t; to thread_pool_base::scheduler fixes the build, but it is not a safe fix. Now the task silently runtime hangs.

    I hit this with exec::task running on exec::asio::asio_thread_pool (also derives from exec::thread_pool_base). With the typedef added, the awaited sender chain runs to completion, but the exec::task coroutine 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:

    stdexec scheduler_concept typedef result
    5473e9da (just before #2038) no compiles, works
    5473e9da yes compiles, hangs
    f91f636 (main) yes compiles, hangs
    f91f636 no does not compile (this issue)

    same stdexec commit, typedef as the only change, working → hung.

    git bisect over my test suite gives b4a598d (#2038) as the first bad commit. My read is that the typedef makes the scheduler claim to satisfy stdexec::scheduler, which switches exec::task into enforcing affinity onto a scheduler that doesn't fully implement the protocol, so completions are dropped.

    So thread_pool_base::scheduler probably 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

  3. ashat1701 commented on Sep 1, 2026

    @ashat1701

    Hi, @mathisloge, did you consider using stdexec::task_scheduler type 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 drop

  4. alwaysprince05 commented on Sep 14, 2026

    @alwaysprince05
    Contributor

    Thanks for the great write-up — the diagnosis was spot on.

    You were right that __semi_scheduler is the culprit: it required a
    scheduler_concept typedef, which static_thread_pool and
    timed_thread_context simply don't define (and don't need to, since the
    public scheduler concept 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions