Skip to content

Commit e0f30a8

Browse files
authored
add counting_scope compatible with alien coroutines (#45)
1 parent 7575785 commit e0f30a8

4 files changed

Lines changed: 272 additions & 0 deletions

File tree

examples/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ add_executable(alien_when_all alien_when_all.cpp)
3232
target_link_libraries(alien_when_all PRIVATE CoroutineTests)
3333
add_executable(alien_sync_wait alien_sync_wait.cpp)
3434
target_link_libraries(alien_sync_wait PRIVATE CoroutineTests)
35+
add_executable(alien_counting_scope alien_counting_scope.cpp)
36+
target_link_libraries(alien_counting_scope PRIVATE CoroutineTests)
3537

3638

3739
if(BUILD_STDEXEC)

examples/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,9 @@ This example demonstrates a `when_all` algorithm compatible with coroutine seman
137137
Link: [alien_sync_wait.cpp](alien_sync_wait.cpp)
138138

139139
This example demonstrates a `sync_wait` algorithm compatible with coroutine semantics as in ["Alien" example](#alien). The algorithm takes a scheduler and coroutine, waits until its completion, and return the results or rethrows an exception.
140+
141+
## Alien counting_scope
142+
143+
Link: [alien_counting_scope.cpp](alien_counting_scope.cpp)
144+
145+
This example demonstrates dynamic work submitting with `counting_scope` compatible with coroutine semantic as in ["Alien" example](#alien). `spawn` schedules execution of a coroutine that returns `void`. `join()` blocks the current thread until all submitted coroutines are finished (and rethrows the first exception captured from submitted work, if any).

examples/alien_counting_scope.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#include <chrono>
2+
#include <coroutine>
3+
#include <string_view>
4+
5+
#include "CoroutineTests/alien/counting_scope.hpp"
6+
#include "CoroutineTests/alien/tool.hpp"
7+
#include "CoroutineTests/threadpool.hpp"
8+
#include "alien_timer.hpp" // AsyncTimer
9+
#include "logging_utils.hpp" // log, format_name
10+
11+
using namespace CoroutineTests::alien;
12+
13+
tool::Task<void> child(std::string_view parent,
14+
std::chrono::milliseconds delay) {
15+
const auto self =
16+
format_name(parent, std::format("child({}ms)", delay.count()));
17+
log(self) << "Starting child" << std::endl;
18+
co_await AsyncTimer{delay, StatusCode::SUCCESS, self};
19+
log(self) << "Finishing child" << std::endl;
20+
co_return;
21+
}
22+
23+
int main() {
24+
log() << "main Starting" << std::endl;
25+
CoroutineTests::Threadpool threadpool(2);
26+
auto scheduler = [&threadpool](std::coroutine_handle<> handle) {
27+
log() << "scheduler Schedule called, enqueuing execution" << std::endl;
28+
threadpool.enqueue_task(handle);
29+
};
30+
31+
CoroutineTests::alien::counting_scope scope;
32+
log() << "main Spawning work" << std::endl;
33+
scope.spawn(scheduler, child("main", std::chrono::milliseconds(20)));
34+
scope.spawn(scheduler, child("main", std::chrono::milliseconds(50)));
35+
scope.spawn(scheduler, child("main", std::chrono::milliseconds(10)));
36+
37+
log() << "main Waiting for scope to become empty" << std::endl;
38+
scope.join();
39+
log() << "main Done" << std::endl;
40+
return 0;
41+
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#ifndef COROUTINETESTS_ALIEN_COUNTING_SCOPE_H
2+
#define COROUTINETESTS_ALIEN_COUNTING_SCOPE_H
3+
4+
#include <atomic>
5+
#include <condition_variable>
6+
#include <coroutine>
7+
#include <exception>
8+
#include <functional>
9+
#include <mutex>
10+
#include <type_traits>
11+
#include <utility>
12+
13+
namespace CoroutineTests::alien {
14+
15+
namespace detail::counting_scope {
16+
17+
// Deduce the result type assuming the type is an awaitable (doesn't check for
18+
// operator co_await or await_transform)
19+
template <typename Coro>
20+
using result_t = decltype(std::declval<Coro>().await_resume());
21+
22+
namespace concepts {
23+
template <typename T>
24+
concept HasScheduler = requires(T t) {
25+
{
26+
t.get_scheduler()
27+
} -> std::convertible_to<std::function<void(std::coroutine_handle<>)>>;
28+
};
29+
30+
template <typename Coro>
31+
concept AlienCoroutine = requires(Coro coro) {
32+
typename Coro::promise_type;
33+
requires HasScheduler<typename Coro::promise_type>;
34+
{ coro.await_resume() };
35+
typename result_t<Coro>;
36+
};
37+
38+
} // namespace concepts
39+
} // namespace detail::counting_scope
40+
41+
// A minimal counting scope compatible with alien-style coroutines.
42+
// Allows to eagerly start multiple tasks (returning void) and wait for their
43+
// completion.
44+
class counting_scope {
45+
public:
46+
counting_scope() = default;
47+
counting_scope(const counting_scope&) = delete;
48+
counting_scope& operator=(const counting_scope&) = delete;
49+
counting_scope(counting_scope&&) = delete;
50+
counting_scope& operator=(counting_scope&&) = delete;
51+
~counting_scope() = default;
52+
53+
// Eagerly start a new task in this scope using the provided scheduler and
54+
// coroutine. The counting_scope keeps track of the number of unfinished
55+
// tasks.
56+
template <typename Coro>
57+
requires detail::counting_scope::concepts::AlienCoroutine<Coro> &&
58+
std::is_same_v<detail::counting_scope::result_t<Coro>, void>
59+
void spawn(std::function<void(std::coroutine_handle<>)> scheduler,
60+
Coro coro) {
61+
m_count += 1;
62+
auto task =
63+
make_detached_task(std::move(coro), *this, std::move(scheduler));
64+
task.start_detached();
65+
}
66+
// Wait for all tasks in this scope to complete and rethrow the first
67+
// exception if any task threw.
68+
void join() {
69+
if (m_count != 0) {
70+
std::unique_lock lock(m_join_mutex);
71+
m_join_cv.wait(lock, [&] { return m_count == 0; });
72+
}
73+
if (auto exception = take_exception()) {
74+
std::rethrow_exception(exception);
75+
}
76+
}
77+
78+
private:
79+
// Store exception unless something is already stored in which case
80+
// keep the original one and ignore the new one
81+
void store_exception(std::exception_ptr exception) noexcept {
82+
if (!exception) {
83+
return;
84+
}
85+
std::scoped_lock lock(m_exception_mutex);
86+
if (!m_exception) {
87+
m_exception = std::move(exception);
88+
}
89+
}
90+
91+
// Take stored exception for rethrowing and clear the storage
92+
std::exception_ptr take_exception() noexcept {
93+
std::scoped_lock lock(m_exception_mutex);
94+
return std::exchange(m_exception, nullptr);
95+
}
96+
97+
// Called by helper tasks when they complete.
98+
// If this was the last unfinished task then notify to stop the wait.
99+
void on_task_complete() noexcept {
100+
if (m_count.fetch_sub(1) != 1) {
101+
return;
102+
}
103+
std::scoped_lock lock(m_join_mutex);
104+
m_join_cv.notify_all();
105+
}
106+
107+
// Helper coroutine type for wrapping other coroutines and notifying the
108+
// counting_scope on completion.
109+
class [[nodiscard]] DetachedTask {
110+
public:
111+
struct promise_type {
112+
counting_scope& m_scope;
113+
std::function<void(std::coroutine_handle<>)> m_scheduler;
114+
115+
// Non-default constructor to pass counting_scope reference and
116+
// scheduler The constructor will be used if coroutine function has
117+
// the same signature The unused parameters are here only to match
118+
// the signature
119+
template <class Coro>
120+
promise_type(
121+
Coro&& /*coro*/, counting_scope& scope,
122+
std::function<void(std::coroutine_handle<>)> scheduler) noexcept
123+
: m_scope(scope), m_scheduler(std::move(scheduler)) {}
124+
125+
// Provide scheduler propagation for alien-style awaitables.
126+
const auto& get_scheduler() const { return m_scheduler; }
127+
// Schedule resumption of this helper
128+
void reschedule() { m_scheduler(handle_type::from_promise(*this)); }
129+
130+
// Required by coroutines: create the object
131+
DetachedTask get_return_object() {
132+
return DetachedTask{handle_type::from_promise(*this)};
133+
}
134+
135+
// Required by coroutines: suspend immediately on start
136+
std::suspend_always initial_suspend() const noexcept { return {}; }
137+
138+
// Required by coroutines: handle completion and resume parent if
139+
// needed
140+
auto final_suspend() const noexcept {
141+
struct final_awaiter {
142+
// Don't skip final suspension
143+
bool await_ready() const noexcept { return false; }
144+
// On suspend, indicate completion and destroy the coroutine
145+
// handle
146+
void await_suspend(handle_type h) const noexcept {
147+
auto& promise = h.promise();
148+
promise.m_scope.on_task_complete();
149+
h.destroy();
150+
}
151+
// Nothing special on resume since the task is already
152+
// complete
153+
void await_resume() const noexcept {}
154+
};
155+
return final_awaiter{};
156+
}
157+
158+
void return_void() noexcept {}
159+
160+
void unhandled_exception() noexcept {
161+
m_scope.store_exception(std::current_exception());
162+
}
163+
};
164+
165+
using handle_type = std::coroutine_handle<promise_type>;
166+
167+
explicit DetachedTask(handle_type h) : m_coroutine(h) {}
168+
DetachedTask() = default;
169+
DetachedTask(const DetachedTask&) = delete;
170+
DetachedTask& operator=(const DetachedTask&) = delete;
171+
DetachedTask(DetachedTask&& other) noexcept
172+
: m_coroutine(std::exchange(other.m_coroutine, {})) {}
173+
DetachedTask& operator=(DetachedTask&& other) noexcept {
174+
if (this != &other) {
175+
if (m_coroutine) {
176+
m_coroutine.destroy();
177+
}
178+
m_coroutine = std::exchange(other.m_coroutine, {});
179+
}
180+
return *this;
181+
}
182+
~DetachedTask() {
183+
if (m_coroutine) {
184+
m_coroutine.destroy();
185+
}
186+
}
187+
188+
// Release the handle and start executing on the scheduler
189+
void start_detached() noexcept {
190+
auto handle = std::exchange(m_coroutine, {});
191+
handle.promise().reschedule();
192+
}
193+
194+
private:
195+
handle_type m_coroutine{};
196+
};
197+
198+
// Create a DetachedTask that wraps the provided coroutine and notifies the
199+
// counting_scope on completion.
200+
template <typename Coro>
201+
requires detail::counting_scope::concepts::AlienCoroutine<Coro> &&
202+
std::is_same_v<detail::counting_scope::result_t<Coro>, void>
203+
static DetachedTask make_detached_task(
204+
Coro awaitable, counting_scope& /*scope*/,
205+
std::function<void(std::coroutine_handle<>)> /*scheduler*/) {
206+
co_await std::move(awaitable);
207+
co_return;
208+
}
209+
210+
private:
211+
std::atomic_size_t m_count{0}; // number of unfinished tasks in this scope
212+
213+
std::mutex m_join_mutex; // coordinates join waiting and task completion
214+
std::condition_variable m_join_cv; // notified when m_count reaches 0
215+
216+
std::mutex m_exception_mutex; // protects m_exception
217+
std::exception_ptr
218+
m_exception; // first exception thrown by any task in this scope
219+
};
220+
221+
} // namespace CoroutineTests::alien
222+
223+
#endif // COROUTINETESTS_ALIEN_COUNTING_SCOPE_H

0 commit comments

Comments
 (0)