Minicoro ist eine einlesige Bibliothek für die Verwendung asymmetrischer Coroutinen in C. Die API ist von Lua-Coroutinen inspiriert, aber unter Berücksichtigung von C-Verwendung.
Das Projekt wird hauptsächlich als Coroutine -Backend für die Nelua -Programmiersprache entwickelt.
Die Implementierung der Bibliotheksbaugruppe ist von Lua Coco von Mike Pall inspiriert.
Die meisten Plattformen werden durch verschiedene Methoden unterstützt:
| Plattform | Montagemethode | Fallback -Methode |
|---|---|---|
| Android | Arm/Arm64 | N / A |
| iOS | Arm/Arm64 | N / A |
| Fenster | x86_64 | Windows -Fasern |
| Linux | x86_64/i686 | Ucontext |
| Mac OS X | x86_64/arm/arm64 | Ucontext |
| WebAssembly | N / A | Emscripten Fasern / Binary Asyncify |
| Raspberry Pi | ARM | Ucontext |
| RISC-V | RV64/RV32 | Ucontext |
Die Montagemethode wird standardmäßig verwendet, wenn sie vom Compiler und der CPU unterstützt wird, da ansonsten die Ucontext- oder Fasermethode als Fallback verwendet wird.
Die Montagemethode ist sehr effizient, es dauert nur ein paar Zyklen, um eine Coroutine zu erzeugen, wieder aufzunehmen, zu ergeben oder zu zerstören.
mco_coro -Objekt ist nicht fadensicher, Sie sollten einen Mutex verwenden, um es in Multithread -Anwendungen zu manipulieren.thread_local Qualifier unterstützt.thread_local im Coroutine -Code zu verwenden, und der Compiler kann lokale Variablenzeiger zwischen Thread -Variablen zwischenspeichern, die bei einem Coroutine -Switch -Thread ungültig sein können.-s ASYNCIFY=1 kompilieren.Eine Coroutine repräsentiert einen unabhängigen "grünen" Ausführungsfaden. Im Gegensatz zu Threads in Multithread -Systemen setzt eine Coroutine jedoch nur die Ausführung aus, indem sie explizit eine Ertragsfunktion aufruft.
Sie erstellen eine Coroutine, indem Sie mco_create anrufen. Sein einziges Argument ist eine mco_desc -Struktur mit einer Beschreibung für die Coroutine. Die mco_create -Funktion erstellt nur eine neue Coroutine und gibt einen Griff zurück, sie startet die Coroutine nicht.
Sie führen eine Coroutine aus, indem Sie mco_resume aufrufen. Wenn Sie eine Lebenslauffunktion aufrufen, startet die Coroutine seine Ausführung, indem Sie seine Körperfunktion aufrufen. Nach dem Laufen der Coroutine läuft sie, bis sie endet oder ergibt.
Eine Coroutine ergibt die Rufe von mco_yield . Wenn eine Korutine ergibt, kehrt der entsprechende Lebenslauf sofort zurück, selbst wenn die Rendite innerhalb der verschachtelten Funktionsaufrufe (dh nicht in der Hauptfunktion) auftritt. Wenn Sie das nächste Mal die gleiche Coroutine wieder aufnehmen, setzt es seine Ausführung von dem Punkt fort, an dem es nachgab.
Um einen anhaltenden Wert mit der Coroutine in Verbindung zu bringen, können Sie user_data optional auf der Erstellung festlegen und später mit mco_get_user_data abrufen.
Um Werte zwischen Lebenslauf und Ertrag zu übergeben, können Sie optional mco_push und mco_pop -APIs verwenden. Sie sollen temporäre Werte mit einem LIFO -Style -Puffer übergeben. Das Speichersystem kann auch zum Senden und Empfangen von Anfangswerten für die Erstellung von Coroutine oder vor dem Abschluss verwendet werden.
Um Minicoro zu verwenden, machen Sie Folgendes in einer .c -Datei:
#define MINICORO_IMPL
#include "minicoro.h" Sie können #include "minicoro.h" in anderen Teilen des Programms genau wie jeder andere Header durchführen.
Das folgende einfache Beispiel zeigt, wie die Bibliothek verwendet wird:
#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 ;
} Hinweis : Falls Sie das Minicoro -Allocator -System nicht verwenden möchten, sollten Sie ein Coroutine -Objekt selbst mit mco_desc.coro_size zuweisen und mco_init anrufen, um später auf den Anruf mco_uninit zu zerstören und es zu beenden.
Sie können die aktuelle Koryutine von überall ausgeben, ohne mco_coro -Zeiger zu übergeben zu müssen, um diese einfach mco_yield(mco_running()) zu verwenden.
Die Bibliothek verfügt über die Speicherschnittstelle, um die Übergabe von Daten zwischen Ertrag und Lebenslauf zu unterstützen. Die Verwendung ist unkompliziert. Verwenden Sie mco_push , um Daten vor einem mco_resume oder mco_yield zu senden, und verwenden Sie später mco_pop nach einem mco_resume oder mco_yield um Daten zu empfangen. Achten Sie darauf, keinen Push -and -Pop -Nichtübereinstimmung zu haben, andernfalls werden diese Funktionen einen Fehler zurückgeben.
Die Bibliotheksrückgabefehlercodes in den meisten Fällen ihrer API im Falle eines Missbrauchs oder des Systemfehlers wird der Benutzer aufgefordert, sie ordnungsgemäß zu verarbeiten.
Die neue Option für Kompilierzeit MCO_USE_VMEM_ALLOCATOR ermöglicht einen virtuellen Speicher -Allocator.
Jede stapelreiche Coroutine muss in der Regel den Speicher für seinen vollständigen Stapel reservieren. Dies macht die Gesamtspeicherverwendung in der Regel sehr hoch, wenn Tausende von Coroutinen zugewiesen werden. Eine Anwendung mit 100 Tausend Coroutine mit Stapeln von 56 KB würde beispielsweise bis zu 5 GB Speicher konsumieren.
Einige Entwickler bevorzugen aufgrund dieses Problems oft stacklose Coroutinen gegenüber stapeligen Coroutinen. Stapelloser Speicher Fußabdruck ist niedrig und ist daher häufig als leichter angesehen. Wie stacklos viele andere Einschränkungen haben, wie Sie nicht eingeschränkten Code in ihnen ausführen können.
Ein Mittel gegen die Lösung besteht darin, stapelful Coroutinen zu wachsen, um nur dann den physischen Speicher bei Bedarf zu verwenden, wenn es wirklich benötigt wird, und es gibt eine gute Möglichkeit, dies auf die virtuelle Speicherzuweisung zu tun, wenn sie vom Betriebssystem unterstützt werden.
Der Virtual Memory Backed Allocator reserviert den virtuellen Speicher im Betriebssystem für jeden Coroutine -Stapel, löst jedoch noch keine echte physische Speicherverwendung aus. Während die Anwendung virtueller Speicherverbrauch hoch ist, ist die physische Speicherverwendung niedrig und wächst tatsächlich auf Bedarf (normalerweise jeden 4 -KB -Stück unter Linux).
Der Virtual Memory Backed Allocator erhöht auch die Standardstapelgröße auf etwa 2 MB, normalerweise die Größe der zusätzlichen Threads unter Linux, sodass Sie mehr Platz in Ihren Coroutinen haben und das Risiko eines Stapelüberlaufs niedrig ist.
Zum Beispiel verwendet die Zuordnung von 100.000 Coroutinen mit fast 2 MB Stack -reserviertem Raum mit dem Virtual Memory Allocator 783 MB physischer Speicherverwendung, das ist etwa 8 KB pro Coroutine, die Verwendung von virtuellen Speicher wird jedoch bei 98 GB liegen.
Es wird empfohlen, diese Option nur zu aktivieren, wenn Sie vorhaben, Tausende von Coroutinen zu erzeugen und gleichzeitig eine geringe Speicher -Fußabdruck zu haben. Nicht alle Umgebungen verfügen über ein Betriebssystem mit virtueller Speicherunterstützung, daher ist diese Option standardmäßig deaktiviert.
Diese Option kann mco_create() / mco_destroy() einen Größenaufwand hinzufügen, da sie das Betriebssystem auffordern, virtuelle Speichertabellen zu verwalten. Wenn dies ein Problem für Sie ist, passen Sie bitte einen benutzerdefinierten Allocator für Ihre eigenen Anforderungen an.
Das Folgende kann definiert werden, um das Bibliotheksverhalten zu ändern:
MCO_API - öffentliche API -Qualifikation. Standard ist extern .MCO_MIN_STACK_SIZE - Mindeststapelgröße beim Erstellen einer Coroutine. Der Standardwert ist 32768 (32 KB).MCO_DEFAULT_STORAGE_SIZE - Größe des Coroutine -Speicherpuffers. Standard ist 1024.MCO_DEFAULT_STACK_SIZE - Standard -Stapelgröße beim Erstellen einer Coroutine. Der Standardwert ist 57344 (56 KB). Wenn MCO_USE_VMEM_ALLOCATOR TRUE ist, ist die Standardeinstellung 2040 KB (fast 2 MB).MCO_ALLOC - Standardzuweisungsfunktion. Standard ist calloc .MCO_DEALLOC - Standard -Deallocation -Funktion. Standard ist free .MCO_USE_VMEM_ALLOCATOR - Verwenden Sie den Virtual Memory Backed Allocator und verbessern Sie den Speicherausdruck pro Coroutine.MCO_NO_DEFAULT_ALLOCATOR - Deaktivieren Sie den Standard -Allocator mit MCO_ALLOC und MCO_DEALLOC .MCO_ZERO_MEMORY - Null Speicher von Stack beim Poping -Speicher, das für müll gesammelte Umgebungen bestimmt ist.MCO_DEBUG - Aktivieren Sie den Debug -Modus und protokollieren Sie einen Laufzeitfehler bei STDOut. Automatisch definiert, es sei denn NDEBUG oder MCO_NO_DEBUG wird definiert.MCO_NO_DEBUG - Deaktivieren Sie den Debug -Modus.MCO_NO_MULTITHREAD - Deaktivieren Sie die Verwendung von Multithread. Multithread wird unterstützt, wenn thread_local unterstützt wird.MCO_USE_ASM - Nutzung der Kontext -Switch -Implementierung der Montage.MCO_USE_UCONTEXT - Nutzung der Ucontext -Kontext -Switch -Implementierung.MCO_USE_FIBERS - Nutzung der Fasernkontext -Switch -Implementierung.MCO_USE_ASYNCIFY - Nutzung von Binary Asyncify Context Switch Implementierung.MCO_USE_VALGRIND - Definieren Sie, ob Sie mit Valgrind ausgeführt werden möchten, um Zugriff auf Speicherfehler zu beheben.Die Coroutine -Bibliothek wurde für x86_64 CPU -Zyklen für den Kontextschalter (ausgelöst in Lebenslauf oder Ertrag) und Initialisierung verhindern.
| CPU Arch | Betriebssystem | Verfahren | Kontextschalter | Initialisieren | Nicht initialisieren |
|---|---|---|---|---|---|
| x86_64 | Linux | Montage | 9 Zyklen | 31 Zyklen | 14 Zyklen |
| x86_64 | Linux | Ucontext | 352 Zyklen | 383 Zyklen | 14 Zyklen |
| x86_64 | Fenster | Fasern | 69 Zyklen | 10564 Zyklen | 11167 Zyklen |
| x86_64 | Fenster | Montage | 33 Zyklen | 74 Zyklen | 14 Zyklen |
HINWEIS : Auf Intel Core i7-8750H CPU @ 2.20GHz mit vorhandenen Coroutinen getestet.
Hier finden Sie eine Liste aller Bibliotheksfunktionen für kurze Referenz:
/* 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. */Das Folgende ist ein umfassenderes Beispiel: Erzeugen von Fibonacci -Nummern:
#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 vor, Tausende von Coroutinen mit niedrigem Speicher-Fußabdruck zuzuordnen. Dies umfasst das Brechen von Änderungen in der Allocator-API.Ich bin ein Vollzeit-Open-Source-Entwickler. Jede Menge der Spende durch meinen GitHub wird geschätzt und könnte mich ermutigen, diese und andere Open-Source-Projekte weiter zu unterstützen. Ich kann einmalige Sponsoring für kleine Funktionen oder geringfügige Verbesserungen akzeptieren, die mit den Projektzielen übereinstimmen, in diesem Fall kontaktieren Sie mich.
Ihre Wahl für öffentlich zugängliche oder mit MIT NO -Zuschreibung siehe Lizenzdatei.