J'essayais de lire la documentation (cppreference et la documentation standard sur la fonctionnalité elle-même) sur la séquence des opérations qui sont appelées lorsqu'une fonction coroutine est appelée, suspendue, reprise et terminée. La documentation présente en détail les différents points d'extension qui permettent aux développeurs de bibliothèques de personnaliser le comportement de leur coroutine à l'aide de composants de bibliothèque. À un niveau élevé, cette fonctionnalité de langue semble être extrêmement bien pensée.
Malheureusement, j'ai beaucoup de mal à suivre les mécanismes de l'exécution de la coroutine et comment, en tant que développeur de bibliothèque, je peux utiliser les différents points d'extension pour personnaliser l'exécution de ladite coroutine. Ou même par où commencer.
Les fonctions suivantes sont dans l'ensemble de nouveaux points de personnalisation que je ne comprends pas complètement:
initial_suspend()
return_void()
return_value()
await_ready()
await_suspend()
await_resume()
final_suspend()
unhandled_exception()
Quelqu'un peut-il décrire dans un pseudo-code de haut niveau, le code que le compilateur génère lors de l'exécution d'une coroutine utilisateur? À un niveau abstrait, j'essaie de comprendre quand des fonctions comme await_suspend
, await_resume
, await_ready
, await_transform
, return_value
, etc. sont appelés, à quoi ils servent et comment je peux les utiliser pour écrire des bibliothèques coroutine.
Je ne sais pas si c'est hors sujet, mais une ressource d'introduction ici serait extrêmement utile pour la communauté en général. Googler et plonger dans les implémentations de bibliothèques comme dans cppcoro ne m'aide pas à dépasser cette barrière initiale :(
N4775 décrit la proposition de coroutines pour C++ 20. Il présente un certain nombre d'idées différentes. Ce qui suit est de mon blog à https://dwcomputersolutions.net . Plus d'informations peuvent être trouvées dans mes autres articles.
Avant d'examiner l'ensemble de notre programme coroutine Hello World, parcourez les différentes parties étape par étape. Ceux-ci inclus:
Le dossier complet est inclus à la fin de ce post.
Future f()
{
co_return 42;
}
Nous instancions notre coroutine avec
Future myFuture = f();
Il s'agit d'une simple coroutine qui renvoie simplement la valeur 42
. C'est une coroutine car elle inclut le mot clé co_return
. Toute fonction possédant les mots clés co_await
, co_return
Ou co_yield
Est une coroutine.
La première chose que vous remarquerez est que bien que nous retournions un entier, le type de retour de la coroutine est le type (défini par l'utilisateur) Future. La raison en est que lorsque nous appelons notre coroutine, nous n'exécutons pas la fonction pour le moment, nous initialisons plutôt un objet qui nous donnera éventuellement la valeur que nous recherchons AKA notre avenir.
Lorsque nous instancions notre coroutine, la première chose que fait le compilateur est de trouver le type de promesse qui représente ce type particulier de coroutine.
Nous indiquons au compilateur quel type de promesse appartient à quelle signature de fonction coroutine en créant une spécialisation partielle de modèle pour
template <typename R, typename P...>
struct coroutine_trait
{};
with a member called `promise_type` that defines our Promise Type
Pour notre exemple, nous pourrions vouloir utiliser quelque chose comme:
template<>
struct std::experimental::coroutines_v1::coroutine_traits<Future> {
using promise_type = Promise;
};
Ici, nous créons une spécialisation de coroutine_trait
Ne spécifie aucun paramètre et un type de retour Future
, cela correspond exactement à notre signature de fonction coroutine Future f(void)
. promise_type
Est alors le type de promesse qui dans notre cas est le struct Promise
.
Maintenant que vous êtes un utilisateur, nous ne créerons normalement pas notre propre spécialisation coroutine_trait
Car la bibliothèque coroutine fournit un moyen simple et agréable de spécifier le promise_type
Dans la classe Future elle-même. Plus sur cela plus tard.
Comme mentionné dans mon post précédent, parce que les coroutines sont suspendues et peuvent être reprises, les variables locales ne peuvent pas toujours être stockées dans la pile. Pour stocker des variables locales non sûres pour la pile, le compilateur alloue un objet Context sur le tas. Une instance de notre promesse sera également stockée.
Les coroutines sont pour la plupart inutiles à moins qu'elles ne soient capables de communiquer avec le monde extérieur. Notre promesse nous dit comment la coroutine devrait se comporter tandis que notre futur objet permet à d'autres codes d'interagir avec la coroutine. La Promesse et l'avenir communiquent ensuite entre eux via notre poignée coroutine.
Une simple promesse coroutine ressemble à ceci:
struct Promise
{
Promise() : val (-1), done (false) {}
std::experimental::coroutines_v1::suspend_never initial_suspend() { return {}; }
std::experimental::coroutines_v1::suspend_always final_suspend() {
this->done = true;
return {};
}
Future get_return_object();
void unhandled_exception() { abort(); }
void return_value(int val) {
this->val = val;
}
int val;
bool done;
};
Future Promise::get_return_object()
{
return Future { Handle::from_promise(*this) };
}
Comme mentionné, la promesse est allouée lorsque la coroutine est instanciée et se termine pendant toute la durée de vie de la coroutine.
Une fois cela fait, le compilateur appelle get_return_object
Cette fonction définie par l'utilisateur est alors responsable de la création de l'objet Future et de sa restitution à l'initiateur coroutine.
Dans notre cas, nous voulons que notre avenir puisse communiquer avec notre coroutine, nous créons donc notre avenir avec le manche de notre coroutine. Cela permettra à notre avenir d'accéder à notre promesse.
Une fois notre coroutine créée, nous devons savoir si nous voulons commencer à l'exécuter immédiatement ou si nous voulons qu'elle reste suspendue immédiatement. Cela se fait en appelant la fonction Promise::initial_suspend()
. Cette fonction renvoie un Awaiter que nous verrons dans un autre article.
Dans notre cas, puisque nous voulons que la fonction démarre immédiatement, nous appelons suspend_never
. Si nous suspendions la fonction, nous aurions besoin de démarrer la coroutine en appelant la méthode resume sur le handle.
Nous devons savoir quoi faire lorsque l'opérateur co_return
Est appelé dans la coroutine. Cela se fait via la fonction return_value
. Dans ce cas, nous stockons la valeur dans la promesse pour une récupération ultérieure via le futur.
En cas d'exception, nous devons savoir quoi faire. Cela se fait par la fonction unhandled_exception
. Étant donné que dans notre exemple, aucune exception ne devrait se produire, nous abandonnons simplement.
Enfin, nous devons savoir quoi faire avant de détruire notre coroutine. Cela se fait via le final_suspend function
Dans ce cas, puisque nous voulons récupérer le résultat, nous retournons donc suspend_always
. La coroutine doit ensuite être détruite via la méthode destroy
de la poignée de la coroutine. Sinon, si nous retournons suspend_never
La coroutine se détruit dès qu'elle a fini de fonctionner.
La poignée donne accès à la coroutine ainsi qu'à sa promesse. Il existe deux versions, la poignée vide lorsque nous n'avons pas besoin d'accéder à la promesse et la poignée coroutine avec le type de promesse lorsque nous devons accéder à la promesse.
template <typename _Promise = void>
class coroutine_handle;
template <>
class coroutine_handle<void> {
public:
void operator()() { resume(); }
//resumes a suspended coroutine
void resume();
//destroys a suspended coroutine
void destroy();
//determines whether the coroutine is finished
bool done() const;
};
template <Promise>
class coroutine_handle : public coroutine_handle<void>
{
//gets the promise from the handle
Promise& promise() const;
//gets the handle from the promise
static coroutine_handle from_promise(Promise& promise) no_except;
};
L'avenir ressemble à ceci:
class [[nodiscard]] Future
{
public:
explicit Future(Handle handle)
: m_handle (handle)
{}
~Future() {
if (m_handle) {
m_handle.destroy();
}
}
using promise_type = Promise;
int operator()();
private:
Handle m_handle;
};
int Future::operator()()
{
if (m_handle && m_handle.promise().done) {
return m_handle.promise().val;
} else {
return -1;
}
}
L'objet Futur est chargé d'abstraire la coroutine au monde extérieur. Nous avons un constructeur qui prend la poignée de la promesse conformément à l'implémentation de la promesse get_return_object
.
Le destructeur détruit la coroutine car dans notre cas c'est l'avenir qui contrôle la durée de vie de la promesse.
enfin nous avons la ligne:
using promise_type = Promise;
La bibliothèque C++ nous évite d'implémenter notre propre coroutine_trait
Comme nous l'avons fait ci-dessus si nous définissons notre promise_type
Dans la classe de retour de la coroutine.
Et nous l'avons. Notre toute première coroutine simple.
#include <experimental/coroutine>
#include <iostream>
struct Promise;
class Future;
using Handle = std::experimental::coroutines_v1::coroutine_handle<Promise>;
struct Promise
{
Promise() : val (-1), done (false) {}
std::experimental::coroutines_v1::suspend_never initial_suspend() { return {}; }
std::experimental::coroutines_v1::suspend_always final_suspend() {
this->done = true;
return {};
}
Future get_return_object();
void unhandled_exception() { abort(); }
void return_value(int val) {
this->val = val;
}
int val;
bool done;
};
class [[nodiscard]] Future
{
public:
explicit Future(Handle handle)
: m_handle (handle)
{}
~Future() {
if (m_handle) {
m_handle.destroy();
}
}
using promise_type = Promise;
int operator()();
private:
Handle m_handle;
};
Future Promise::get_return_object()
{
return Future { Handle::from_promise(*this) };
}
int Future::operator()()
{
if (m_handle && m_handle.promise().done) {
return m_handle.promise().val;
} else {
return -1;
}
}
//The Co-routine
Future f()
{
co_return 42;
}
int main()
{
Future myFuture = f();
std::cout << "The value of myFuture is " << myFuture() << std::endl;
return 0;
}
L'opérateur co_await
Nous permet de suspendre notre coroutine et de retourner le contrôle à l'appelant coroutine. Cela nous permet de faire d'autres travaux en attendant la fin de notre opération. Quand ils ont terminé, nous pouvons les reprendre exactement là où nous nous étions arrêtés.
Il existe plusieurs façons pour l'opérateur co_await
De traiter l'expression à sa droite. Pour l'instant, nous considérerons le cas le plus simple et c'est là que notre expression co_await
Renvoie un Awaiter.
Un Awaiter est un simple struct
ou class
qui implémente les méthodes suivantes: await_ready
, await_suspend
Et await_resume
.
bool await_ready() const {...}
renvoie simplement si nous sommes prêts à reprendre notre coroutine ou si nous devons envisager de suspendre notre coroutine. En supposant que await_ready
Renvoie faux. Nous continuons à exécuter await_suspend
Plusieurs signatures sont disponibles pour la méthode await_suspend
. Le plus simple est void await_suspend(coroutine_handle<> handle) {...}
. Il s'agit du handle de l'objet coroutine que notre co_await
Suspendra. Une fois cette fonction terminée, le contrôle est renvoyé à l'appelant de l'objet coroutine. C'est cette fonction qui est chargée de stocker la poignée de la coroutine pour plus tard afin que notre coroutine ne reste pas suspendue pour toujours.
Une fois que handle.resume()
est appelée; await_ready
Renvoie false; ou un autre mécanisme reprend notre coroutine, la méthode auto await_resume()
est appelée. La valeur de retour de await_resume
Est la valeur renvoyée par l'opérateur co_await
. Parfois, il est impossible pour expr dans co_await expr
De renvoyer un serveur comme décrit ci-dessus. Si expr
renvoie une classe, la classe peut fournir sa propre instance de Awaiter operator co_await (...) which will return the Awaiter. Alternatively one can implement an
wait_transform method in our
Promise_type` qui transformera expr dans un Awaiter.
Maintenant que nous avons décrit Awaiter, je voudrais souligner que les méthodes initial_suspend
Et final_suspend
Dans nos promise_type
Renvoient toutes deux Awaiters. L'objet suspend_always
Et suspend_never
Sont des attentes triviales. suspend_always
Renvoie vrai à await_ready
Et suspend_never
Renvoie faux. Cependant, rien ne vous empêche de déployer le vôtre.
Si vous êtes curieux de savoir à quoi ressemble un Awaiter dans la vraie vie, jetez un œil à mon futur objet . Il stocke la poignée coroutine dans une lamda pour un traitement ultérieur.