web-dev-qa-db-fra.com

Quelles sont les mécaniques des coroutines en C ++ 20?

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 :(

23
Curious

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:

  1. La promesse coroutine
  2. Le contexte de la coroutine
  3. L'avenir de la coroutine
  4. La poignée coroutine
  5. La coroutine elle-même
  6. Le sous-programme qui utilise réellement la coroutine

Le dossier complet est inclus à la fin de ce post.

La Coroutine

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.

Trouver le type promis

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.

Le contexte Coroutine

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.

La promesse, l'avenir et la poigné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.

La promesse

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

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

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.

Source complète



#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;
}

Attendants

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.

15
doron