En quoi les coroutines empilées diffèrent-elles des coroutines empilées?

Contexte:

je demande cela parce que j'ai actuellement une application avec beaucoup (des centaines à des milliers) de threads. La plupart de ces threads sont inactifs une grande partie du temps, attendant que les éléments de travail soient placés dans une file d'attente. Lorsqu'un élément de travail est disponible, il est alors traité en appelant du code existant complexe de façon arbitraire. Sur certaines configurations de système d'exploitation, l'application se heurte aux paramètres du noyau régissant le nombre maximum d'utilisateurs processus, donc je voudrais expérimenter des moyens pour réduire le nombre de fils de travail.

Ma solution proposée:

il semble qu'une approche basée sur la coroutine, où je remplace chaque fil ouvrier par une coroutine, pourrait aider à accomplir ceci. Je peux alors avoir une file d'attente de travail supportée par un pool de threads de travail réels (noyau). Lorsqu'un article est placé dans la file d'attente d'une coroutine particulière pour traitement, une entrée est placée dans la file d'attente du groupe de fils. Il reprendrait alors la coroutine correspondante, traiterait ses données en file d'attente, puis la suspendrait à nouveau, libérant le thread worker pour faire un autre travail.

détails de mise en oeuvre:

en réfléchissant à la façon dont je ferais cela, j'ai de la difficulté à comprendre les différences fonctionnelles entre les coroutines empilées et empilées. J'ai une certaine expérience de l'utilisation de coroutines empilées en utilisant le coup de pouce.Coroutine bibliothèque. Je trouve que c'est relativement facile à comprendre à partir d'un niveau conceptuel: pour chaque coroutine, il maintient une copie du contexte CPU et de la pile, et lorsque vous passez à une coroutine, il passe à ce contexte sauvegardé (comme le ferait un planificateur en mode noyau).

ce qui est moins clair pour moi, c'est la différence entre une coroutine empilée et ceci. Dans ma demande, le montant des frais généraux associés à la file d'attente décrite ci-dessus est très important. La plupart des implémentations que j'ai vu, comme le nouveau CO2 bibliothèque suggèrent que les coroutines stackless fournissent des commutateurs de contexte beaucoup plus bas.

par conséquent, je voudrais comprendre plus clairement les différences fonctionnelles entre les coroutines empilées et empilées. Plus précisément, je pense à ces questions:

  • Références comme celui-ci suggérez que la distinction réside dans l'endroit où vous pouvez céder/reprendre dans une corotine empilable vs. empilable. Est-ce le cas? Est-il un exemple simple de quelque chose que je peux faire dans une Corentine empilée mais pas dans une Corentine sans empilement?

  • y a-t-il des limites à l'utilisation des variables de stockage automatique (c.-à-d. Les variables "sur la pile")?

  • y a-t-il des limites aux fonctions que je peux appeler à partir d'une coroutine sans piles?

  • S'il n'y a pas de sauvegarde du contexte stack pour une coroutine stackless, où vont les variables de stockage automatique lorsque la coroutine est en cours d'exécution?

44
demandé sur Jason R 2015-03-11 04:57:33

2 réponses

tout d'Abord, merci de prendre un coup d'oeil à CO2:)

Le Boost.Coroutine doc décrit bien l'avantage de la coroutine empilée:

stackfulness

par opposition à une coroutine sans empilement une corotine empilée peut être suspendu à l'intérieur d'un ensemble structure de pile. L'exécution reprend à exactement le même point dans le code où il a été suspendu avant. Avec un stackless coroutine, seule la routine de haut niveau peut être suspendue. Toute routine appelée par cette routine de haut niveau ne peut pas elle-même être suspendue. Ceci interdit de suspendre/reprendre les opérations dans les routines une bibliothèque polyvalente.

suite de première classe

une continuation de première classe peut être passée comme un argument, retourné par une fonction et stocké dans une structure de données à être utilisé plus tard. Dans certaines implémentations (par exemple C# rendement), la la poursuite ne peut pas être directement accessibles ou manipulé directement.

sans empilement et sémantique de première classe, une certaine exécution utile les flux de contrôle ne peuvent pas être pris en charge (par exemple les flux coopératifs). multitâche ou Checkpoint).

Ce que cela signifie pour vous? par exemple, imaginez que vous avez une fonction qui prend un visiteur:

template<class Visitor>
void f(Visitor& v);

vous voulez le transformer en itérateur, avec la coroutine empilée, vous pouvez:

asymmetric_coroutine<T>::pull_type pull_from([](asymmetric_coroutine<T>::push_type& yield)
{
    f(yield);
});

mais avec la coroutine stackless, il n'y a aucun moyen de le faire:

generator<T> pull_from()
{
    // yield can only be used here, cannot pass to f
    f(???);
}

en général, la coroutine empilée est plus puissante que la coroutine empilée. Alors pourquoi voulons-nous de la corotine sans empilement? réponse courte: l'efficacité.

coroutine empilable a généralement besoin d'allouer une certaine quantité de mémoire pour accommoder sa pile d'exécution (doit être assez grande), et le commutateur de contexte est plus coûteux que le commutateur sans empilement, par exemple Boost.Coroutine prend 40 cycles alors que le CO2 ne prend que 7 cycles en moyenne sur ma machine, parce que la seule chose qu'une coroutine stackless doit restaurer est le compteur de programme.

cela dit, avec le support du langage, coroutine empilable peut probablement aussi profiter de la taille max calculée par le compilateur pour la pile aussi longtemps qu'il n'y a pas de récursion dans la coroutine, donc l'utilisation de la mémoire peut aussi être améliorée.

en parlant de coroutine stackless, gardez à l'esprit que cela ne signifie pas que il n'y a pas de pile runtime-stack du tout, cela signifie seulement qu'il utilise la même pile runtime que le côté hôte, donc vous pouvez aussi appeler des fonctions récursives, juste que toutes les récursions se produiront sur la pile runtime-stack de l'hôte. En revanche, avec la coroutine empilée, quand vous appelez fonctions récursives, les récursions se produiront sur la propre pile de la coroutine.

Pour répondre à ces questions:

  • y a-t-il des limites à l'utilisation des variables de stockage automatique? (c'est à dire des variables "sur la pile")?

Non. C'est la limitation de L'émulation du CO2. Avec le soutien de langue, les variables de stockage automatique visible à la coroutine sera placé sur le stockage interne de la coroutine. Notez mon accent sur "visible à la coroutine", si la coroutine appelle une fonction qui utilise des variables de stockage automatiques en interne, alors ces variables seront placées sur la pile runtime-stack. Plus précisément, la coroutine stackless ne doit préserver que les variables / temporairesqui peuvent être utilisées après la reprise.

pour être clair, vous pouvez utiliser des variables de stockage automatique dans le corps coroutine de CO2 aussi bien:

auto f() CO2_RET(co2::task<>, ())
{
    int a = 1; // not ok
    CO2_AWAIT(co2::suspend_always{});
    {
        int b = 2; // ok
        doSomething(b);
    }
    CO2_AWAIT(co2::suspend_always{});
    int c = 3; // ok
    doSomething(c);
} CO2_END

tant que la définition ne précède pas tout await.

  • y a-t-il des limites aux fonctions que je peux appeler à partir d'un coroutine stackless?

Non.

  • s'il n'y a pas de sauvegarde du contexte stack pour une coroutine stackless, où puis-automatique les variables de stockage vont quand la coroutine est en cours d'exécution?

réponse ci-dessus, une coroutine stackless ne se soucie pas des variables de stockage automatiques utilisées dans les fonctions appelées, elles seront simplement placées sur la pile d'exécution normale.

si vous avez le moindre doute, vérifiez simplement le code source de CO2, il peut vous aider à comprendre la mécanique sous le capot ;)

41
répondu Jamboree 2015-03-11 15:23:27

ce que vous voulez, ce sont des fils/fibres de l'utilisateur-land - habituellement vous voulez suspendre votre code (en cours d'exécution en fibre) dans une pile d'appels imbriquée (par exemple parsing messages from TCP-connection). Dans ce cas, vous ne pouvez pas utiliser la commutation de contexte stackless (la pile d'application est partagée entre les coroutines stackless -> les cadres de la pile de sous-routines appelées seraient écrasés).

vous pouvez utiliser quelque chose comme boost.fibre qui met en œuvre l'utilisateur-terre fils/fibres basées sur boost.cadre.

2
répondu olk 2015-03-12 10:25:37