Minicoro est une bibliothèque unique pour utiliser des coroutines asymétriques dans C. L'API est inspirée par Lua Coroutines mais avec une utilisation C à l'esprit.
Le projet est principalement développé pour être un backend Coroutine pour le langage de programmation NELUA.
L'implémentation de l'assemblage de la bibliothèque est inspirée par Lua Coco par Mike Pall.
La plupart des plateformes sont prises en charge par différentes méthodes:
| Plate-forme | Méthode d'assemblage | Méthode de secours |
|---|---|---|
| Androïde | ARM / ARM64 | N / A |
| ios | ARM / ARM64 | N / A |
| Fenêtre | x86_64 | Fibres Windows |
| Linux | x86_64 / i686 | ucontex |
| Mac OS X | x86_64 / arm / arm64 | ucontex |
| Webassembly | N / A | Emscripten Fibers / Binaryen Asyncify |
| Raspberry Pi | BRAS | ucontex |
| RISC-V | RV64 / RV32 | ucontex |
La méthode d'assemblage est utilisée par défaut si elle est prise en charge par le compilateur et le processeur, sinon la méthode UContext ou fibre est utilisée comme repli.
La méthode d'assemblage est très efficace, il ne faut que quelques cycles pour créer, reprendre, céder ou détruire une coroutine.
mco_coro n'est pas sûr, vous devez utiliser un mutex pour le manipuler dans des applications multithread.thread_local Qualifier.thread_local à l'intérieur, le compilateur peut mettre en cache des pointeurs de variables locales qui peuvent être invalides lorsqu'ils sont des filetages de l'interrupteur à coroutine.-s ASYNCIFY=1 .Une coroutine représente un fil d'exécution "vert" indépendant. Contrairement aux threads dans les systèmes Multithread, une coroutine ne suspend pas son exécution en appelant explicitement une fonction de rendement.
Vous créez une coroutine en appelant mco_create . Son seul argument est une structure mco_desc avec une description de la coroutine. La fonction mco_create ne crée qu'une nouvelle coroutine et y retourne une poignée, elle ne démarre pas la coroutine.
Vous exécutez une coroutine en appelant mco_resume . Lors de l'appel d'une fonction de CV, la coroutine démarre son exécution en appelant sa fonction corporelle. Une fois que la coroutine a commencé à courir, elle s'exécute jusqu'à ce qu'elle se termine ou donne.
Une coroutine donne en appelant mco_yield . Lorsqu'une coroutine donne, le CV correspondant revient immédiatement, même si le rendement se produit à l'intérieur des appels de fonction imbriqués (c'est-à-dire pas dans la fonction principale). La prochaine fois que vous reprenez la même coroutine, elle continue son exécution du point où elle a cédé.
Pour associer une valeur persistante à la Coroutine, vous pouvez éventuellement définir user_data sur sa création et récupérer plus tard avec mco_get_user_data .
Pour passer les valeurs entre le CV et le rendement, vous pouvez éventuellement utiliser les API mco_push et mco_pop , ils sont destinés à passer des valeurs temporaires à l'aide d'un tampon de style LIFO. Le système de stockage peut également être utilisé pour envoyer et recevoir des valeurs initiales sur la création de Coroutine ou avant sa fin.
Pour utiliser Minicoro, procédez comme suit dans un fichier .c:
#define MINICORO_IMPL
#include "minicoro.h" Vous pouvez faire #include "minicoro.h" dans d'autres parties du programme comme tout autre en-tête.
L'exemple simple suivant montre comment utiliser la bibliothèque:
#define MINICORO_IMPL
#include "minicoro.h"
#include <stdio.h>
#include <assert.h>
// Coroutine entry function.
void coro_entry ( mco_coro * co ) {
printf ( "coroutine 1n" );
mco_yield ( co );
printf ( "coroutine 2n" );
}
int main () {
// First initialize a `desc` object through `mco_desc_init`.
mco_desc desc = mco_desc_init ( coro_entry , 0 );
// Configure `desc` fields when needed (e.g. customize user_data or allocation functions).
desc . user_data = NULL ;
// Call `mco_create` with the output coroutine pointer and `desc` pointer.
mco_coro * co ;
mco_result res = mco_create ( & co , & desc );
assert ( res == MCO_SUCCESS );
// The coroutine should be now in suspended state.
assert ( mco_status ( co ) == MCO_SUSPENDED );
// Call `mco_resume` to start for the first time, switching to its context.
res = mco_resume ( co ); // Should print "coroutine 1".
assert ( res == MCO_SUCCESS );
// We get back from coroutine context in suspended state (because it's unfinished).
assert ( mco_status ( co ) == MCO_SUSPENDED );
// Call `mco_resume` to resume for a second time.
res = mco_resume ( co ); // Should print "coroutine 2".
assert ( res == MCO_SUCCESS );
// The coroutine finished and should be now dead.
assert ( mco_status ( co ) == MCO_DEAD );
// Call `mco_destroy` to destroy the coroutine.
res = mco_destroy ( co );
assert ( res == MCO_SUCCESS );
return 0 ;
} Remarque : Si vous ne souhaitez pas utiliser le système d'allocateur Minicoro, vous devez allouer un objet Coroutine vous-même à l'aide mco_desc.coro_size et d'appeler mco_init , puis plus tard pour détruire l'appel mco_uninit et le transformer.
Vous pouvez donner la coroutine en cours d'exécution actuelle de n'importe où sans avoir à passer des pointeurs mco_coro autour, pour cela utiliser simplement mco_yield(mco_running()) .
La bibliothèque a l'interface de stockage pour aider à passer les données entre le rendement et le curriculum vitae. Son utilisation est simple, utilisez mco_push pour envoyer des données avant un mco_resume ou mco_yield , puis utilisez plus tard mco_pop après un mco_resume ou mco_yield pour recevoir des données. Prenez soin de ne pas distinguer une poussée et une pop, sinon ces fonctions renvoient une erreur.
Les codes d'erreur de retour de la bibliothèque dans la majeure partie de son API en cas d'erreur abusive ou système, l'utilisateur est encouragé à les gérer correctement.
La nouvelle option de temps de compilation MCO_USE_VMEM_ALLOCATOR permet un allocateur soutenu par la mémoire virtuelle.
Chaque coroutine de pile doit généralement réserver de la mémoire pour sa pile complète, ce qui rend généralement l'utilisation totale de la mémoire lors de l'allocation de milliers de coroutines, par exemple, une application avec 100 milliers de coroutine avec des piles de 56 Ko consommerait jusqu'à 5 Go de mémoire, mais votre application peut ne pas vraiment d'utiliser la pile complète pour chaque coroutine.
Certains développeurs préfèrent souvent les coroutines sans pile aux coroutines piles en raison de ce problème, l'empreinte de la mémoire sans empilement est faible, donc souvent considérée comme plus légère. Cependant, Stackless ont de nombreuses autres limitations, comme vous ne pouvez pas exécuter de code sans contrainte à l'intérieur.
Un remède à la solution consiste à rendre les coroutines de pile grandissables, pour utiliser la mémoire physique à la demande que lorsqu'elle est vraiment nécessaire, et il existe une belle façon de le faire en s'appuyant sur une allocation de mémoire virtuelle lorsqu'elle est prise en charge par le système d'exploitation.
L'allocateur soutenu par la mémoire virtuelle réservera la mémoire virtuelle dans le système d'exploitation pour chaque pile Coroutine, mais ne déclenchera pas encore de réelle utilisation de la mémoire physique. Bien que l'utilisation de la mémoire virtuelle de l'application soit élevée, l'utilisation de la mémoire physique sera faible et se développera en réalité à la demande (généralement tous les morceaux de 4 kb en Linux).
L'allocateur soutenu par la mémoire virtuelle augmente également la taille de pile par défaut à environ 2 Mo, généralement la taille des threads supplémentaires dans Linux, vous avez donc plus d'espace dans vos coroutines et le risque de débordement de pile est faible.
Par exemple, l'allocation de 100 milliers de coroutines avec près de 2 Mo de pile d'espace avec l'allocateur de mémoire virtuelle utilise 783 Mo d'utilisation de la mémoire physique, soit environ 8 Ko par coroutine, mais l'utilisation de la mémoire virtuelle sera à 98 Go.
Il est recommandé d'activer cette option que si vous prévoyez de pulvériser des milliers de coroutines tout en souhaitant avoir une empreinte de mémoire basse. Tous les environnements n'ont pas un système d'exploitation avec prise en charge de la mémoire virtuelle, donc cette option est désactivée par défaut.
Cette option peut ajouter une surcharge d'ordre de magnitude à mco_create() / mco_destroy() , car ils demanderont au système d'exploitation de gérer les tables de page de mémoire virtuelles, s'il s'agit d'un problème pour vous, veuillez personnaliser un allocateur personnalisé pour vos propres besoins.
Les éléments suivants peuvent être définis pour modifier le comportement de la bibliothèque:
MCO_API - Qualificateur public API. La valeur par défaut est extern .MCO_MIN_STACK_SIZE - Taille de pile minimale lors de la création d'une coroutine. La valeur par défaut est 32768 (32KB).MCO_DEFAULT_STORAGE_SIZE - Taille du tampon de stockage Coroutine. La valeur par défaut est 1024.MCO_DEFAULT_STACK_SIZE - Taille de pile par défaut lors de la création d'une coroutine. La valeur par défaut est 57344 (56KB). Lorsque MCO_USE_VMEM_ALLOCATOR est vrai, la valeur par défaut est 2040KB (près de 2 Mo).MCO_ALLOC - Fonction d'allocation par défaut. La valeur par défaut est calloc .MCO_DEALLOC - Fonction de transformation par défaut. La valeur par défaut est free .MCO_USE_VMEM_ALLOCATOR - Utilisez l'allocateur soutenu par la mémoire virtuelle, améliorant l'empreinte de la mémoire par coroutine.MCO_NO_DEFAULT_ALLOCATOR - Désactivez l'allocateur par défaut à l'aide MCO_ALLOC et MCO_DEALLOC .MCO_ZERO_MEMORY - Mémoire zéro de la pile lors de la pause du stockage, destiné aux environnements collectés à la poubelle.MCO_DEBUG - Activer le mode de débogage, enregistrer toute erreur d'exécution sur stdout. Défini automatiquement à moins que NDEBUG ou MCO_NO_DEBUG ne soit défini.MCO_NO_DEBUG - Désactiver le mode de débogage.MCO_NO_MULTITHREAD - Désactivez l'utilisation de multithread. Multithread est pris en charge lorsque thread_local est pris en charge.MCO_USE_ASM - Force Utilisation de la mise en œuvre du commutateur de contexte d'assemblage.MCO_USE_UCONTEXT - Force Utilisation de l'implémentation du commutateur de contexte UContext.MCO_USE_FIBERS - Force Utilisation of Fibers Context Switch Implementation.MCO_USE_ASYNCIFY - Utilisation de la force de l'implémentation de commutateur de contexte Asyncify Binaryen.MCO_USE_VALGRIND - Définissez si vous souhaitez exécuter avec Valgrind pour corriger les erreurs de mémoire d'accès.La bibliothèque Coroutine a été comparée pour les cycles de processeur de comptage x86_64 pour le commutateur de contexte (déclenché dans le curriculum vitae ou le rendement) et l'initialisation.
| Arche du processeur | OS | Méthode | Commutateur de contexte | Initialiser | Désinitialiser |
|---|---|---|---|---|---|
| x86_64 | Linux | assemblée | 9 cycles | 31 cycles | 14 cycles |
| x86_64 | Linux | ucontex | 352 cycles | 383 cycles | 14 cycles |
| x86_64 | Fenêtre | fibres | 69 cycles | 10564 cycles | 11167 cycles |
| x86_64 | Fenêtre | assemblée | 33 cycles | 74 cycles | 14 cycles |
Remarque : Testé sur Intel Core i7-8750H CPU @ 2,20 GHz avec des coroutines pré-allouées.
Voici une liste de toutes les fonctions de bibliothèque pour une référence rapide:
/* Structure used to initialize a coroutine. */
typedef struct mco_desc {
void ( * func )( mco_coro * co ); /* Entry point function for the coroutine. */
void * user_data ; /* Coroutine user data, can be get with `mco_get_user_data`. */
/* Custom allocation interface. */
void * ( * alloc_cb )( size_t size , void * allocator_data ); /* Custom allocation function. */
void ( * dealloc_cb )( void * ptr , size_t size , void * allocator_data ); /* Custom deallocation function. */
void * allocator_data ; /* User data pointer passed to `alloc`/`dealloc` allocation functions. */
size_t storage_size ; /* Coroutine storage size, to be used with the storage APIs. */
/* These must be initialized only through `mco_init_desc`. */
size_t coro_size ; /* Coroutine structure size. */
size_t stack_size ; /* Coroutine stack size. */
} mco_desc ;
/* Coroutine functions. */
mco_desc mco_desc_init ( void ( * func )( mco_coro * co ), size_t stack_size ); /* Initialize description of a coroutine. When stack size is 0 then MCO_DEFAULT_STACK_SIZE is used. */
mco_result mco_init ( mco_coro * co , mco_desc * desc ); /* Initialize the coroutine. */
mco_result mco_uninit ( mco_coro * co ); /* Uninitialize the coroutine, may fail if it's not dead or suspended. */
mco_result mco_create ( mco_coro * * out_co , mco_desc * desc ); /* Allocates and initializes a new coroutine. */
mco_result mco_destroy ( mco_coro * co ); /* Uninitialize and deallocate the coroutine, may fail if it's not dead or suspended. */
mco_result mco_resume ( mco_coro * co ); /* Starts or continues the execution of the coroutine. */
mco_result mco_yield ( mco_coro * co ); /* Suspends the execution of a coroutine. */
mco_state mco_status ( mco_coro * co ); /* Returns the status of the coroutine. */
void * mco_get_user_data ( mco_coro * co ); /* Get coroutine user data supplied on coroutine creation. */
/* Storage interface functions, used to pass values between yield and resume. */
mco_result mco_push ( mco_coro * co , const void * src , size_t len ); /* Push bytes to the coroutine storage. Use to send values between yield and resume. */
mco_result mco_pop ( mco_coro * co , void * dest , size_t len ); /* Pop bytes from the coroutine storage. Use to get values between yield and resume. */
mco_result mco_peek ( mco_coro * co , void * dest , size_t len ); /* Like `mco_pop` but it does not consumes the storage. */
size_t mco_get_bytes_stored ( mco_coro * co ); /* Get the available bytes that can be retrieved with a `mco_pop`. */
size_t mco_get_storage_size ( mco_coro * co ); /* Get the total storage size. */
/* Misc functions. */
mco_coro * mco_running ( void ); /* Returns the running coroutine for the current thread. */
const char * mco_result_description ( mco_result res ); /* Get the description of a result. */Ce qui suit est un exemple plus complet, générant des numéros de Fibonacci:
#define MINICORO_IMPL
#include "minicoro.h"
#include <stdio.h>
#include <stdlib.h>
static void fail ( const char * message , mco_result res ) {
printf ( "%s: %sn" , message , mco_result_description ( res ));
exit ( -1 );
}
static void fibonacci_coro ( mco_coro * co ) {
unsigned long m = 1 ;
unsigned long n = 1 ;
/* Retrieve max value. */
unsigned long max ;
mco_result res = mco_pop ( co , & max , sizeof ( max ));
if ( res != MCO_SUCCESS )
fail ( "Failed to retrieve coroutine storage" , res );
while ( 1 ) {
/* Yield the next Fibonacci number. */
mco_push ( co , & m , sizeof ( m ));
res = mco_yield ( co );
if ( res != MCO_SUCCESS )
fail ( "Failed to yield coroutine" , res );
unsigned long tmp = m + n ;
m = n ;
n = tmp ;
if ( m >= max )
break ;
}
/* Yield the last Fibonacci number. */
mco_push ( co , & m , sizeof ( m ));
}
int main () {
/* Create the coroutine. */
mco_coro * co ;
mco_desc desc = mco_desc_init ( fibonacci_coro , 0 );
mco_result res = mco_create ( & co , & desc );
if ( res != MCO_SUCCESS )
fail ( "Failed to create coroutine" , res );
/* Set storage. */
unsigned long max = 1000000000 ;
mco_push ( co , & max , sizeof ( max ));
int counter = 1 ;
while ( mco_status ( co ) == MCO_SUSPENDED ) {
/* Resume the coroutine. */
res = mco_resume ( co );
if ( res != MCO_SUCCESS )
fail ( "Failed to resume coroutine" , res );
/* Retrieve storage set in last coroutine yield. */
unsigned long ret = 0 ;
res = mco_pop ( co , & ret , sizeof ( ret ));
if ( res != MCO_SUCCESS )
fail ( "Failed to retrieve coroutine storage" , res );
printf ( "fib %d = %lun" , counter , ret );
counter = counter + 1 ;
}
/* Destroy the coroutine. */
res = mco_destroy ( co );
if ( res != MCO_SUCCESS )
fail ( "Failed to destroy coroutine" , res );
return 0 ;
}MCO_USE_VMEM_ALLOCATOR pour allouer des milliers de coroutines avec une empreinte de mémoire faible, cela inclut les changements de rupture de l'API d'allocateur.Je suis un développeur open source à plein temps, tout montant de don via mon github sera apprécié et pourrait m'encourager pour continuer à soutenir ce projets et d'autres projets open source. Je peux accepter des parrainages ponctuels pour de petites fonctionnalités ou des améliorations mineures alignées sur les objectifs du projet, dans ce cas, contactez-moi.
Votre choix de domaine public ou MIT sans attribution, voir le fichier de licence.