Dans cet article, nous analyserons en détail le concept de coroutines, leur classification, examinerons de plus près l'implémentation, les hypothèses et les compromis proposés par le nouveau standard C ++ 20.
informations générales
Les coroutines peuvent être vues comme une généralisation du concept de routines (fonctions) en termes d'opérations effectuées sur elles. La différence fondamentale entre les coroutines et les sous-programmes est qu'une coroutine offre la possibilité de suspendre explicitement son exécution, de donner le contrôle à d'autres unités de programme et de reprendre son travail au même moment où le contrôle est repris, en utilisant des opérations supplémentaires, tout en conservant les données locales (état d'exécution). entre les appels successifs, offrant ainsi un flux de contrôle plus flexible et plus étendu.
Pour clarifier cette définition et approfondir le raisonnement et introduire des concepts et des termes auxiliaires, considérons la mécanique des fonctions ordinaires en C ++ et leur nature de pile.
Nous considérerons la sémantique d'une fonction dans le cadre de deux opérations.
(call). . :
- — (activation record, activation frame), ;
- ( ) , ;
- . ;
- — , .
, .
(return). . :
- ( ) ;
- , ;
- .
, . ().
:
- (strictly nested lifetime) . , : . .
- .
, . — , : ss ( ), bp ( ), sp ( ), ( ). , , .
. , (Calling Convention). , . ( ) . , , , .
:
void bar(int a, int b)
{}
void foo()
{
int a = 1;
int b = 2;
bar(a, b);
}
int main()
{
foo();
}
- (x86-64 clang 10.0.0 -m32,
32 . 64 , , , ):
bar(int, int):
push ebp
mov ebp, esp
mov eax, dword ptr [ebp + 12]
mov ecx, dword ptr [ebp + 8]
pop ebp
ret
foo():
push ebp
mov ebp, esp
sub esp, 24
mov dword ptr [ebp - 4], 1
mov dword ptr [ebp - 8], 2
mov eax, dword ptr [ebp - 4]
mov ecx, dword ptr [ebp - 8]
mov dword ptr [esp], eax
mov dword ptr [esp + 4], ecx
call bar(int, int)
add esp, 24
pop ebp
ret
main:
push ebp
mov ebp, esp
sub esp, 8
call foo()
xor eax, eax
add esp, 8
pop ebp
ret
:
main
, , ebp
( ) .. , ebp
esp
( )
| ... |
+----------------+
| return address |
+----------------+
| saved rbp | <-- ebp, esp
+----------------+
foo
. . .. 16 8 (4 4 ebp
) 8 , .
| ... |
+----------------+
| return address |
+----------------+
| saved rbp | <-- ebp
+----------------+
| ... |
| 8 byte padding |
| ... | <-- esp
-----------------+
foo. call
. foo ebp
( ) ebp esp
( ), .
| ... |
+----------------+
| return address |
+----------------+
| saved rbp | <-- ebp
+----------------+
| ... |
| 8 byte padding |
| ... |
+----------------+
| return address |
+----------------+
| saved rbp | <-- ebp, esp
+----------------+
bar
. int
— 8 , bar
int
— 8 . .. 8 ( ebp
) 8 . 8 + 8 + 8 = 24
, .
| ... |
+----------------+
| return address |
+----------------+
| saved rbp | <-- ebp
+----------------+
| ... |
| 8 byte padding |
| ... |
+----------------+
| return address |
+----------------+
| saved rbp | <-- ebp
+----------------+
| local a | <-- ebp - 4
+----------------+
| local b | <-- ebp - 8
+----------------+
| ... |
| 8 byte padding |
| ... |
+----------------+
| arg a | <-- esp + 4
+----------------+
| arg b | <-- esp
+----------------+
bar
. , foo
. call
. bar
ebp
ebp
esp
, .
| ... |
+----------------+
| return address |
+----------------+
| saved rbp | <-- ebp
+----------------+
| ... |
| 8 byte padding |
| ... |
+----------------+
| return address |
+----------------+
| saved rbp | <-- ebp
+----------------+
| local a | <-- ebp - 4
+----------------+
| local b | <-- ebp - 8
+----------------+
| ... |
| 8 byte padding |
| ... |
+----------------+
| arg a | <-- ebp + 12
+----------------+
| arg b | <-- ebp + 8
+----------------+
| return address |
+----------------+
| saved rbp | <-- ebp, esp
+----------------+
bar
. ebp
( , foo
) , 4 . 4 . foo
.
| ... |
+----------------+
| return address |
+----------------+
| saved rbp | <-- ebp
+----------------+
| ... |
| 8 byte padding |
| ... |
+----------------+
| return address |
+----------------+
| saved rbp | <-- ebp
+----------------+
| local a | <-- ebp - 4
+----------------+
| local b | <-- ebp - 8
+----------------+
| ... |
| 8 byte padding |
| ... |
+----------------+
| arg a | <-- esp + 4
+----------------+
| arg b | <-- esp
+----------------+
foo
bar
. , 24 .
| ... |
+----------------+
| return address |
+----------------+
| saved rbp | <-- ebp
+----------------+
| ... |
| 8 byte padding |
| ... |
+----------------+
| return address |
+----------------+
| saved rbp | <-- ebp, esp
+----------------+
ebp
( , main
) . . main
.
| ... |
+----------------+
| return address |
+----------------+
| saved rbp | <-- ebp
+----------------+
| ... |
| 8 byte padding |
| ... | <-- esp
-----------------+
main
, , .
| ... |
+----------------+
| return address |
+----------------+
, , .
, .
- ;
- ;
- ( ).
(symmetric) (asymmetric, semi-symmetric).
, , . : , . , , , .
: , , .
.
A , .
(first-class object, first-class citizen) (constrained, compiler-internal), (handles), .
— , , , ( ). , , . , (function object): , , .
(stackful) (stackless). , , (proccesor stack).
:
- (Application stack).
main
. . , ; - (Thread stack). . ( 1-2 );
- (Side stack). (Execution context) , (top level context function, ) . ( ), . : , , .
. — ( , : ) . , - , , .
, c: getcontext
, makecontext
swapcontext
(. Complete Context Control)
#include <iostream>
#include <ucontext.h>
static ucontext_t caller_context;
static ucontext_t coroutine_context;
void print_hello_and_suspend()
{
// Hello
std::cout << "Hello";
// ,
// caller_context
// coroutine_context ,
// , .
swapcontext(&coroutine_context, &caller_context);
}
void simple_coroutine()
{
// coroutine_context
//
// print_hello_and_suspend.
print_hello_and_suspend();
// print_hello_and_suspend
// Coroutine! ,
// ,
// coroutine_context.uc_link, .. caller_context
std::cout << "Coroutine!" << std::endl;
}
int main()
{
// .
char stack[256];
// coroutine_context
// uc_link caller_context, .
// uc_stack
coroutine_context.uc_link = &caller_context;
coroutine_context.uc_stack.ss_sp = stack;
coroutine_context.uc_stack.ss_size = sizeof(stack);
getcontext(&coroutine_context);
// coroutine_context
// ,
// simple_coroutine
makecontext(&coroutine_context, simple_coroutine, 0);
// , coroutine_context
// caller_context ,
// , .
swapcontext(&caller_context, &coroutine_context);
//
//
std::cout << " ";
// .
swapcontext(&caller_context, &coroutine_context);
return 0;
}
, Boost: Boost.Coroutine, Boost.Coroutine2, Boost ucontext_t
fcontext_t
— , (, / , ) POSIX .
, , , . , , , , . , , , , .. . .
:
- (top level function), ;
- ;
- , , ;
- ( ), , .
, C++20, , , .
C++20.
C++ Coroutine TS. Coroutine TS , , .
. range based for, , , begin
end
, , , . , , .
compile-internal .
, , . , -, , , , -, ( ), .
, , .. .
, C++20 compile-internal asymmetric stackless coroutines.
, , .
New Keywords.
:
- co_await. , , , , ;
- co_yield. , co_await, ;
- co_return. , , .
, .
:
-
main
; -
return
; -
constexpr
; - (auto);
- (variadic arguments, variadic templates);
- ;
- .
User types.
, .
Promise.
Promise . :
- ;
- ;
- ;
- co_await;
- .
promise new delete, .
promise .
Promise std::coroutine_traits
, : , , , . std::coroutine_traits
:
template <typename Ret, typename = std::void_t<>>
struct coroutine_traits_base
{};
template <typename Ret>
struct coroutine_traits_base<Ret, std::void_t<typename Ret::promise_type>>
{
using promise_type = typename Ret::promise_type;
};
template <typename Ret, typename... Ts>
struct coroutine_traits : coroutine_traits_base<Ret>
{};
promise_type. std::coroutine_traits
, , promise_type
. promise_type
, .
Promise .
struct Task
{
struct Promise
{
...
};
using promise_type = Promise;
};
...
Task foo()
{
...
}
Task
, : , () .
Promise — std::coroutine_traits
. , ,
class Coroutine
{
public:
void call(int);
};
namespace std
{
template<>
struct coroutine_traits<void, Coroutine, int>
{
using promise_type = Coroutine;
};
}
Promise , , . , lvalues, Promise .. . Promise .
Promise, : Awaitable.
Awaitable.
Awaitable . :
- , co_await;
- ( );
- co_await, .
Awaitable (overload resolution) co_await. , Awaitable. .
, , , , .
Task foo()
{
using namespace std::chrono_literals;
//
//
co_await 10s;
// 10 .
}
std::chrono::duration<long long>
, co_await .
template<typename Rep, typename Period>
auto operator co_await(std::chrono::duration<Rep, Period> duration) noexcept
{
struct Awaitable
{
explicit Awaitable(std::chrono::system_clock::duration<Rep, Period> duration)
: duration_(duration)
{}
...
private:
std::chrono::system_clock::duration duration_;
};
return Awaitable{ duration };
}
Awaitable, , .
Awaitable, co_await <expr>
, .
{
// Promise
using coroutine_traits = std::coroutine_traits<ReturnValue, Args...>;
using promise_type = typename coroutine_traits::promise_type;
...
// co_await <expr>
// 1.
// Awaitable, co_await,
// (
// Promise),
// .. Awaitable , .
frame->awaitable = create_awaitable(<expr>);
// 2.
// await_ready().
//
//
// , .
if (!awaitable.await_ready())
{
// 3.
// await_ready() false,
// ,
// : ,
// ( ,
// ,
// <resume-point>)
<suspend-coroutine>
// 4.
// coroutine_handle
// corotine_handle - .
// :
// ( ) .
using handle_type = std::coroutine_handle<promise_type>;
using await_suspend_result_type =
decltype(frame->awaitable.await_suspend(handle_type::from_promise(promise)));
// 5.
// await_suspend(handle),
// await_suspend
//
// ( ).
// - .
// ,
if constexpr (std::is_void_v<await_suspend_result_type>)
{
// void,
//
// ( ,
// )
frame->awaitable.await_suspend(handle_type::from_promise(promise));
<return-to-caller-or-resumer>;
}
else if constexpr (std::is_same_v<await_suspend_result_type, bool>)
{
// bool,
// false,
//
// , ,
// Awaitable
if (frame->awaitable.await_suspend(handle_type::from_promise(promise))
<return-to-caller-or-resumer>;
}
else if constexpr (is_coroutine_handle_v<await_suspend_result_type>)
{
// std::coroutine_handle<OtherPromise>,
// .. ,
// ,
//
//
auto&& other_handle = frame->awaitable.await_suspend(
handle_type::from_promise(promise));
other_handle.resume();
}
else
{
static_assert(false);
}
}
// 6.
// ()
// await_resume(). .
// co_await.
resume_point:
return frame->awaitable.await_resume();
}
:
- , , co_await. , , ;
- ,
await_suspend
. . , - . ,await_suspend
.await_suspend
.await_resume
, Awaitable . Promise ,await_suspend
. ,await_suspend
: (this ) Promise, .
Awaitable type-traits :
// std::coroutine_handle
template<typename Type>
struct is_coroutine_handle : std::false_type
{};
template<typename Promise>
struct is_coroutine_handle<std::coroutine_handle<Promise>> : std::true_type
{};
// await_suspend
// - void
// - bool
// - std::coroutine_handle
template<typename Type>
struct is_valid_await_suspend_return_type : std::disjunction<
std::is_void<Type>,
std::is_same<Type, bool>,
is_coroutine_handle<Type>>
{};
// await_suspend
template<typename Type>
using is_await_suspend_method = is_valid_await_suspend_return_type<
decltype(std::declval<Type>().await_suspend(std::declval<std::coroutine_handle<>>()))>;
// await_ready
template<typename Type>
using is_await_ready_method = std::is_constructible<bool, decltype(
std::declval<Type>().await_ready())>;
// Awaitable
/*
templae<typename Type>
struct Awaitable
{
...
bool await_ready();
void await_suspend(std::coroutine_handle<>);
Type await_resume();
...
}
*/
template<typename Type, typename = std::void_t<>>
struct is_awaitable : std::false_type
{};
template<typename Type>
struct is_awaitable<Type, std::void_t<
decltype(std::declval<Type>().await_ready()),
decltype(std::declval<Type>().await_suspend(std::declval<std::coroutine_handle<>>())),
decltype(std::declval<Type>().await_resume())>> : std::conjunction<
is_await_ready_method<Type>,
is_await_suspend_method<Type>>
{};
template<typename Type>
constexpr bool is_awaitable_v = is_awaitable<Type>::value;
:
template<typename Rep, typename Period>
auto operator co_await(std::chrono::duration<Rep, Period> duration) noexcept
{
struct Awaitable
{
explicit Awaitable(std::chrono::system_clock::duration duration)
: duration_(duration)
{}
bool await_ready() const noexcept
{
return duration_.count() <= 0;
}
void await_resume() noexcept
{}
void await_suspend(std::coroutine_handle<> h)
{
// timer::async .
// ,
// callback.
timer::async(duration_, [h]()
{
h.resume();
});
}
private:
std::chrono::system_clock::duration duration_;
};
return Awaitable{ duration };
}
// ,
Task tick()
{
using namespace std::chrono_literals;
co_await 1s;
std::cout << "1..." << std::endl;
co_await 1000ms;
std::cout << "2..." << std::endl;
}
int main()
{
tick();
std::cin.get();
}
-
tick
; - co_await Awaitable, 1 ;
-
await_ready
, ; -
tick
, ; -
await_suspend
; - await_suspend
timer::async
,callback
.callback
; - —
main
; -
main
get
, , . , ; - ,
callback
, , ; -
resume
. :tick
, , ; -
await_resume
Awaitable, co_await ; -
await_resume
, co_await , ; -
tick
"1..."; - co_await. 2. ,
main
, a ,callback
, .. resumer'. ; -
tick
( )
co_await Awaitable, Promise .
Promise.
Awaitable, Promise , .
:
- . . .. ;
- - (
co_awat
/co_yield
/co_return
). , .. ; - , . .
// .
//
// 1. resume - ,
// , -.
// 2. promise - Promise
// 3. state -
// 4. heap_allocated -
//
// 5. args -
// 6. locals -
// ...
struct coroutine_frame
{
void (*resume)(coroutine_frame *);
promise_type promise;
int16_t state;
bool heap_allocated;
// args
// locals
//...
};
// 1. . .
template<typename ReturnValue, typename ...Args>
ReturnValue Foo(Args&&... args)
{
// 1.
// Promise
using coroutine_traits = std::coroutine_traits<ReturnValue, Args...>;
using promise_type = typename coroutine_traits::promise_type;
// 2.
// .
//
// Promise,
// , ,
// .
// 1. promise_type
// get_return_object_on_allocation_failure,
// new,
// get_return_object_on_allocation_failure,
// .
// 2. new.
coroutine_frame* frame = nullptr;
if constexpr (has_static_get_return_object_on_allocation_failure_v<promise_type>)
{
frame = reinterpret_cast<coroutine_frame*>(
operator new(__builtin_coro_size(), std::nothrow));
if(!frame)
return promise_type::get_return_object_on_allocation_failure();
}
else
{
frame = reinterpret_cast<coroutine_frame*>(operator new(__builtin_coro_size()));
}
// 3.
// .
// .
// (lvalue rvalue) .
<move-args-to-frame>
// 4.
// promise_type
new(&frame->promise) create_promise<promise_type>(<frame-lvalue-args>);
// 5.
// Promise::get_return_object().
//
// .
// ,
// .. (. co_await).
auto return_object = frame->promise.get_return_object();
// 6.
// -
//
// GCC, ,
// ramp-fucntion ( )
// action-function ( -)
void couroutine_states(coroutine_frame*);
couroutine_states(frame);
// 7.
// ,
// ,
//
// - couroutine_states, .
return return_object;
}
Promise new
delete. , , new
delete
:
struct Promise
{
void* operator new(std::size_t size, std::nothrow_t) noexcept
{
...
}
void operator delete(void* ptr, std::size_t size)
{
...
}
//
static auto get_return_object_on_allocation_failure() noexcept
{
//
return make_invalid_task();
}
};
new
c , . , , leading-allocator convention.
// Promise new c
template<typename Allocator>
struct Promise : PromiseBase
{
// std::allocator_arg_t - tag-
//
void* operator new(std::size_t size, std::allocator_arg_t, Allocator allocator) noexcept
{
...
}
void operator delete(void* ptr, std::size_t size)
{
...
}
};
// std::coroutine_traits
namespace std
{
template<typename... Args>
struct coroutine_traits<Task, Args...>
{
using promise_type = PromiseBase;
};
template<typename Allocator>
struct coroutine_traits<Task, std::allocator_arg_t, Allocator>
{
using promise_type = Promise<Allocator>;
};
}
//
int main()
{
MyAlloc alloc;
coro(std::allocator_arg, alloc);
...
}
, new
delete
, .
. .
-, . , , , . , , , Undefined Behavior.
void Coroutine(const std::vector<int>& data)
{
co_await 10s;
for(const auto& value : data)
std::cout << value << std::endl;
}
void Foo()
{
// 1. vector<int>;
// 2. data;
// 3. , ..
// , , data
// ;
// 4. ;
// 5. , vector<int>,
// , co_await
// Foo ;
// 6. 10 ,
// c , , data
// ( , ), .
Coroutine({1, 2, 3});
...
}
Promise. , , , , Promise ( ) , , . , Promise - , , Promise, .
Promise get_return_object
. , . : get_return_object
. , , - , . onadic composition.
class Task
{
public:
struct promise_type
{
auto get_return_object() noexcept
{
return Task{ std::coroutine_handle<promise_type>::from_promise(*this) };
}
...
};
void resume()
{
if(coro_handle)
coro_handle.resume();
}
private:
Task() = default;
explicit Task(std::coroutine_handle<> handle)
: coro_handle(handle)
{}
std::coroutine_handle<> coro_handle;
};
std::coroutine_handle
— , : ( ) . from_promise
, Promise.
couroutine_states
-, couroutine_states
, , get_return_object
.
State Machine.
couroutine_states
- co_awat/co_yield/co_return : , resume
. .
void couroutine_states(coroutine_frame* frame)
{
switch(frame->state)
{
case 0:
... goto resume_point_0;
case N:
goto resume_point_N;
...
}
co_await promise.initial_suspend();
try
{
// function body
}
catch(...)
{
promise.unhandled_exception();
}
final_suspend:
co_await promise.final_suspend();
}
, co_await, resume_point
— .
{
...
resume_point:
return frame->awaitable.await_resume();
}
co_await — co_await, state
, . — , , co_await .
initial_suspend
co_await. : . Awaitable: std::suspend_never
, std::suspend_always
, initial_suspend
, .
namespace std
{
struct suspend_never
{
bool await_ready() noexcept { return true; }
void await_suspend(coroutine_handle<>) noexcept {}
void await_resume() noexcept {}
};
struct suspend_always
{
bool await_ready() noexcept { return false; }
void await_suspend(coroutine_handle<>) noexcept {}
void await_resume() noexcept {}
};
}
// ,
//
class Task
{
public:
struct promise_type
{
...
auto init_suspend() const noexcept
{
return std::suspend_never{};
}
}
...
};
// ,
//
// ,
// resume.
class TaskManual
{
public:
struct promise_type
{
...
auto init_suspend() const noexcept
{
return std::suspend_always{};
}
}
...
};
. , try-catch
unhandled_exception
.
, co_await, co_yield co_return. . Promise, .
co_yield <expr>
:
co_await frame->promise.yield_value(<expr>);
.. Promise . yield_value
:
template<typename Type>
class Task
{
public:
struct promise_type
{
...
// C ,
// ,
// co_await std::suspend_always.
auto yield_value(Type value)
{
current_value = std::move(value);
return std::suspend_always{};
}
};
...
};
Task
Promise
. co_yield
— . cppcoro
co_return return
. .
co_rturn :
// co_return; frame->promise.return_void(); goto final_suspend;
, ,
void
, co_rturn
// co_return <expr>; frame->promise.return_value(<expr>); goto final_suspend;
void
,
// co_return <expr>; <expr>; frame->promise.return_void(); goto final_suspend;
, co_return, co_return;
. .. frame->promise.return_void()
.
Promise return_value
return_void
, final_suspend
.
initial_suspend
, final_suspend
. , .
// ,
// . Promise ,
// ,
// delete, .
// Undefined Behavior.
// , .
class Task
{
public:
struct promise_type
{
...
auto final_suspend() const noexcept
{
//
return std::suspend_never{};
}
};
...
};
// ,
// .
// Undefined Behavior.
//
// coroutine_handle::destroy()
// , ,
// Promise.
class TaskManual
{
public:
struct promise_type
{
...
auto final_suspend() const noexcept
{
//
return std::suspend_always{};
}
}
...
};
co_await . init_suspend
final_suspend
, co_yield, . . Promise await_transform
, co_await
// co_await <expr>
co_await frame->promise.await_transform(<expr>);
, , co_await , Awaitable, . co_await
.
class Task
{
public:
struct promise_type
{
...
template<typename Type>
auto await_transform(Type&& Whatever) const noexcept
{
static_assert(false,
"co_await is not supported in coroutines of type Generator");
return std::suspend_never{};
}
};
...
};
:
:
:
- Working Draft, Standard for Programming Language C++ (N4830);
- Coroutine TS (N4760);
- First-class symmetric coroutines in C++;
- Core Coroutines;
- Low-level API for stackful coroutines;
- A low-level API for stackful context switching;
- Coroutines: Use-cases and Trade-offs;
- Revisiting Coroutines;
- Coroutines empilables et fonctions de reprise sans pile ;
- Fonctions pouvant être reprises ;
- Expressions avec reprise ;
- Coroutines dans LLVM ;
- Boost Coroutine2 ;
Je serais heureux d'avoir des commentaires et des suggestions (vous pouvez envoyer un courriel à yegorov.alex@gmail.com)
Merci!