Minicoro es una biblioteca de un solo archivo para usar coroutinas asimétricas en C. La API está inspirada en Lua Coroutines pero con C Use en mente.
El proyecto se está desarrollando principalmente para ser un backend de Coroutine para el lenguaje de programación Nelua.
La implementación de la Asamblea de la Biblioteca está inspirada en Lua Coco por Mike Pall.
La mayoría de las plataformas son compatibles con diferentes métodos:
| Plataforma | Método de ensamblaje | Método de retroceso |
|---|---|---|
| Androide | ARM/ARM64 | N / A |
| iOS | ARM/ARM64 | N / A |
| Windows | x86_64 | Fibras de Windows |
| Linux | x86_64/i686 | UCONTEXT |
| Mac OS X | X86_64/ARM/ARM64 | UCONTEXT |
| Aviso web | N / A | Fibras emscripten / binaryen asyncify |
| Frambuesa pi | BRAZO | UCONTEXT |
| RISC-V | RV64/RV32 | UCONTEXT |
El método de ensamblaje se usa de forma predeterminada si es compatible con el compilador y la CPU, de lo contrario, el método de fibra o el método de fibra se utiliza como alternativa.
El método de ensamblaje es muy eficiente, solo toma algunos ciclos crear, reanudar, ceder o destruir una coroutina.
mco_coro no es seguro, debe usar un mutex para manipularlo en aplicaciones múltiples.thread_local .thread_local , el compilador puede almacenar en caché los punteros de variables locales que pueden ser inválidas cuando un interruptor de coroutine se eleva.-s ASYNCIFY=1 .Una coroutina representa un hilo de ejecución "verde" independiente. Sin embargo, a diferencia de los hilos en los sistemas multiproceso, una coroutina solo suspende su ejecución al llamar explícitamente una función de rendimiento.
Creas una coroutina llamando mco_create . Su único argumento es una estructura mco_desc con una descripción para la coroutina. La función mco_create solo crea una nueva coroutina y le devuelve un mango, no comienza la coroutina.
Ejecuta una coroutine llamando mco_resume . Al llamar a una función de currículum, la coroutina comienza su ejecución llamando a su función corporal. Después de que la coroutina comienza a funcionar, se ejecuta hasta que termina o rendir.
Una coroutina produce llamando a mco_yield . Cuando una coroutina produce, el currículum correspondiente regresa inmediatamente, incluso si el rendimiento ocurre dentro de las llamadas de función anidada (es decir, no en la función principal). La próxima vez que reanude la misma coroutina, continúa su ejecución desde el punto en que cedió.
Para asociar un valor persistente con el Coroutine, opcionalmente puede configurar user_data en su creación y luego recuperar con mco_get_user_data .
Para pasar valores entre el currículum y el rendimiento, puede usar opcionalmente las API mco_push y mco_pop , están destinados a pasar valores temporales utilizando un búfer de estilo LIFO. El sistema de almacenamiento también se puede usar para enviar y recibir valores iniciales en la creación de Coroutine o antes de que termine.
Para usar minicoro, haga lo siguiente en un archivo .c:
#define MINICORO_IMPL
#include "minicoro.h" Puede hacer #include "minicoro.h" en otras partes del programa al igual que cualquier otro encabezado.
El siguiente ejemplo simple demuestra cómo usar la biblioteca:
#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 ;
} Nota : En caso de que no desee usar el sistema de asignación de minicoro, debe asignar un objeto Coroutine usted mismo usando mco_desc.coro_size y llamar mco_init , luego para destruir mco_uninit y desearlo.
Puede producir la coroutina actual de ejecución desde cualquier lugar sin tener que pasar los punteros mco_coro , a esto solo use mco_yield(mco_running()) .
La biblioteca tiene la interfaz de almacenamiento para ayudar a pasar datos entre el rendimiento y el currículum. Su uso es sencillo, use mco_push para enviar datos antes de un mco_resume o mco_yield , luego luego use mco_pop después de un mco_resume o mco_yield para recibir datos. Tenga cuidado de no incorporar un empuje y pop, de lo contrario, estas funciones devolverán un error.
Los códigos de error de retorno de la biblioteca En la mayoría de su API en caso de mal uso o error del sistema, se alienta al usuario a manejarlos correctamente.
La nueva opción de tiempo de compilación MCO_USE_VMEM_ALLOCATOR habilita un asignador de memoria virtual respaldada.
Cada coroutina llena generalmente tiene que reservar la memoria para su pila completa, esto generalmente hace que el uso total de la memoria sea muy alto al asignar miles de coroutinas, por ejemplo, una aplicación con 100 miles de coroutina con pilas de 56 kb consumiría tan alto como 5 GB de memoria, sin embargo, su aplicación puede no realmente el uso total de Stack para cada corutina.
Algunos desarrolladores a menudo prefieren las coroutinas sin pilas sobre las corutinas apiladas debido a este problema, la huella de memoria sin pilas es baja, por lo tanto, a menudo se considera más liviana. Sin embargo, Stackless tiene muchas otras limitaciones, como no se puede ejecutar código sin restricciones dentro de ellas.
Un remedio para la solución es hacer que las coroutinas de pila sean desarrolladas, solo usar la memoria física a pedido cuando realmente es necesario, y hay una buena manera de hacer esto dependiendo de la asignación de memoria virtual cuando el sistema operativo es compatible con el sistema operativo.
El asignador respaldado por memoria virtual reservará la memoria virtual en el sistema operativo para cada pila de coroutine, pero aún no activará el uso real de la memoria física. Si bien el uso de la memoria virtual de la aplicación será alto, el uso de la memoria física será bajo y realmente crecerá a pedido (generalmente cada fragmento de 4KB en Linux).
El asignador respaldado por memoria virtual también eleva el tamaño de pila predeterminado a aproximadamente 2 MB, generalmente del tamaño de los hilos adicionales en Linux, por lo que tiene más espacio en sus corutinas y el riesgo de desbordamiento de la pila es bajo.
Como ejemplo, la asignación de 100 miles de corutinas con un espacio reservado de la pila de casi 2 MB con el asignador de memoria virtual usa 783 MB de uso de memoria física, es decir, aproximadamente 8 kb por coroutina, sin embargo, el uso de memoria virtual estará a 98 GB.
Se recomienda habilitar esta opción solo si planea generar miles de corutinas mientras desea tener una huella de memoria baja. No todos los entornos tienen un sistema operativo con soporte de memoria virtual, por lo tanto, esta opción está deshabilitada de forma predeterminada.
Esta opción puede agregar un orden de sobrecarga de magnitud a mco_create() / mco_destroy() , porque solicitarán al sistema operativo que administre las tablas de página de memoria virtual, si este es un problema para usted, personalice un asignador personalizado para sus propias necesidades.
Lo siguiente se puede definir para cambiar el comportamiento de la biblioteca:
MCO_API - Calificador de API pública. El valor predeterminado es extern .MCO_MIN_STACK_SIZE - Tamaño mínimo de la pila al crear una coroutina. El valor predeterminado es 32768 (32kb).MCO_DEFAULT_STORAGE_SIZE - Tamaño del búfer de almacenamiento de Coroutine. El valor predeterminado es 1024.MCO_DEFAULT_STACK_SIZE - Tamaño de pila predeterminado al crear una coroutine. El valor predeterminado es 57344 (56 kb). Cuando MCO_USE_VMEM_ALLOCATOR es verdadero, el valor predeterminado es 2040kb (casi 2mb).MCO_ALLOC - Función de asignación predeterminada. El valor predeterminado es calloc .MCO_DEALLOC : función de desasos predeterminada. El valor predeterminado es free .MCO_USE_VMEM_ALLOCATOR - Utilice el asignador de memoria virtual respaldada por la memoria, mejorando la huella de memoria por coroutina.MCO_NO_DEFAULT_ALLOCATOR - Desactive el asignador predeterminado usando MCO_ALLOC y MCO_DEALLOC .MCO_ZERO_MEMORY - Memoria cero de la pila Al explotar el almacenamiento, destinado a entornos recolectados de basura.MCO_DEBUG - Habilitar el modo de depuración, registrando cualquier error de tiempo de ejecución a StDOut. Definido automáticamente a menos que se define NDEBUG o MCO_NO_DEBUG .MCO_NO_DEBUG - Desactivar el modo de depuración.MCO_NO_MULTITHREAD - Desactivar el uso de múltiplesread. Multithread se admite cuando se admite thread_local .MCO_USE_ASM - Uso de fuerza de implementación del interruptor de contexto de ensamblaje.MCO_USE_UCONTEXT - Force el uso de la implementación del interruptor de contexto de UContext.MCO_USE_FIBERS - Fuerza el uso de la implementación del interruptor de contexto de las fibras.MCO_USE_ASYNCIFY - Force el uso de la implementación del interruptor de contexto de Binaryen Asyncify.MCO_USE_VALGRIND - Defina si desea ejecutar con Valgrind para corregir los errores de memoria de acceso.La biblioteca de Coroutine se comparó para los ciclos de CPU de contexto x86_64 para el interruptor de contexto (activado en currículum o rendimiento) e inicialización.
| Arco de la CPU | Sistema operativo | Método | Interruptor de contexto | Inicializar | Desinicio |
|---|---|---|---|---|---|
| x86_64 | Linux | asamblea | 9 ciclos | 31 ciclos | 14 ciclos |
| x86_64 | Linux | UCONTEXT | 352 ciclos | 383 ciclos | 14 ciclos |
| x86_64 | Windows | fibras | 69 ciclos | 10564 ciclos | 11167 ciclos |
| x86_64 | Windows | asamblea | 33 ciclos | 74 ciclos | 14 ciclos |
Nota : Probado en la CPU de Intel Core i7-8750H @ 2.20 GHz con coroutinas previas a la asignación.
Aquí hay una lista de todas las funciones de la biblioteca para referencia rápida:
/* 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. */El siguiente es un ejemplo más completo, generando números 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 para asignar miles de corutinas con baja huella de memoria, esto incluye los cambios de ruptura en la API de asignador.Soy un desarrollador de código abierto a tiempo completo, cualquier cantidad de donación a través de mi GitHub será apreciado y podría traerme aliento para seguir apoyando este y otros proyectos de código abierto. Puedo aceptar patrocinios únicos para características pequeñas o mejoras menores alineadas con los objetivos del proyecto, en este caso contactarme.
Su elección de dominio público o MIT sin atribución, consulte el archivo de licencia.