O Minicoro é uma biblioteca de arquivo único para o uso de coroutinas assimétricas em C. A API é inspirada nas coroutinas Lua, mas com o uso de C.
O projeto está sendo desenvolvido principalmente para ser um back -end de coroutina para a linguagem de programação de Nelua.
A implementação do conjunto da biblioteca é inspirada em Lua Coco, de Mike Pall.
A maioria das plataformas é suportada através de diferentes métodos:
| Plataforma | Método de montagem | Método de fallback |
|---|---|---|
| Android | ARM/ARM64 | N / D |
| iOS | ARM/ARM64 | N / D |
| Windows | x86_64 | Fibras do Windows |
| Linux | x86_64/i686 | uContext |
| Mac OS X. | x86_64/ARM/ARM64 | uContext |
| WebAssembly | N / D | Fibras emscripten / binaryen assarir |
| Raspberry Pi | BRAÇO | uContext |
| Risc-v | RV64/RV32 | uContext |
O método de montagem é usado por padrão se suportado pelo compilador e pela CPU, caso contrário, o método uContext ou fibra é usado como fallback.
O método de montagem é muito eficiente, apenas leva alguns ciclos para criar, retomar, produzir ou destruir uma coroutina.
mco_coro não é seguro, você deve usar um mutex para manipulá -lo em aplicativos multithread.thread_local .thread_local , o compilador pode cache o encadeamento de variáveis locais ponteiros que podem ser inválidos quando um switch coroutine threads.-s ASYNCIFY=1 .Uma coroutina representa um fio "verde" independente de execução. Ao contrário dos threads em sistemas multithread, no entanto, uma coroutina apenas suspende sua execução chamando explicitamente uma função de rendimento.
Você cria uma coroutina ligando para mco_create . Seu único argumento é uma estrutura mco_desc com uma descrição para a coroutina. A função mco_create cria apenas uma nova coroutina e retorna uma alça, ela não inicia a coroutina.
Você executa uma coroutina chamando mco_resume . Ao chamar uma função de currículo, a coroutina inicia sua execução chamando sua função corporal. Depois que a coroutina começa a funcionar, ela é executada até que termine ou renda.
Uma coroutina produz ligando para mco_yield . Quando uma coroutina produz, o currículo correspondente retorna imediatamente, mesmo que o rendimento aconteça dentro de chamadas de função aninhada (ou seja, não na função principal). Na próxima vez que você retomar a mesma coroutina, ela continua sua execução a partir do ponto em que produziu.
Para associar um valor persistente à coroutina, você pode opcionalmente definir user_data em sua criação e depois recuperar com mco_get_user_data .
Para passar os valores entre o currículo e o rendimento, você pode opcionalmente usar as APIs mco_push e mco_pop , eles devem passar valores temporários usando um buffer de estilo LIFO. O sistema de armazenamento também pode ser usado para enviar e receber valores iniciais na criação de coroutine ou antes de terminar.
Para usar o Minicoro, faça o seguinte em um arquivo .C:
#define MINICORO_IMPL
#include "minicoro.h" Você pode fazer #include "minicoro.h" em outras partes do programa, como qualquer outro cabeçalho.
O exemplo simples a seguir demonstra como usar a 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 : Caso você não queira usar o sistema Minicoro Alocator, você deve alocar um objeto de coroutina usando mco_desc.coro_size e CHAMADO mco_init , depois para destruir a chamada mco_uninit e desalocudar -o.
Você pode produzir a coroutina de corrida atual de qualquer lugar sem precisar passar por volta mco_coro por aí, para isso apenas usar mco_yield(mco_running()) .
A biblioteca possui a interface de armazenamento para ajudar a transmitir dados entre rendimento e currículo. O uso é direto, use mco_push para enviar dados antes de um mco_resume ou mco_yield e depois use mco_pop após um mco_resume ou mco_yield para receber dados. Tome cuidado para não incompatir um empurrão e pop, caso contrário, essas funções retornarão um erro.
Os códigos de erro de retorno da biblioteca na maioria de sua API em caso de uso indevido ou erro do sistema, o usuário é incentivado a lidar com eles corretamente.
A nova opção de tempo de compilação MCO_USE_VMEM_ALLOCATOR permite um alocador de memória virtual.
Toda corotação empilhada geralmente precisa reservar memória para sua pilha completa, isso normalmente torna o uso total da memória muito alto ao alocar milhares de coroutinas, por exemplo, um aplicativo com 100 milhares de coroutina com pilhas de 56kb consumiria tão alto quanto 5 GB de memória, no entanto, seu aplicativo pode não ser realmente um uso de pilha completa para todas as corotas.
Alguns desenvolvedores geralmente preferem coroutinas sem empilhamento a coroutinas empilhadas por causa desse problema, a pegada de memória sem pilha é baixa, portanto, muitas vezes considerada mais leve. No entanto, a pilha sem muitas outras limitações, como você não pode executar código sem restrições dentro delas.
Um remédio para a solução é tornar o Coroutines empilhados crescer, usar apenas a memória física sob demanda quando é realmente necessária, e há uma boa maneira de fazer isso dependendo da alocação de memória virtual quando suportado pelo sistema operacional.
O alocador backup de memória virtual reservará a memória virtual no sistema operacional para cada pilha de coroutina, mas ainda não acionará o uso real da memória física. Embora o uso da memória virtual do aplicativo seja alto, o uso da memória física será baixa e realmente crescerá sob demanda (geralmente a cada pedaço de 4KB no Linux).
O alocador de memória virtual também aumenta o tamanho da pilha padrão para cerca de 2 MB, normalmente o tamanho dos threads extras no Linux, para que você tenha mais espaço em suas coroutinas e o risco de transbordamento de pilha é baixo.
Como exemplo, alocando 100 milhares de coroutinas com espaço reservado para pilha de quase 2 MB com o alocador de memória virtual usa 783 MB de uso de memória física, ou seja, cerca de 8kb por coroutina, no entanto, o uso da memória virtual estará em 98 GB.
Recomenda -se ativar esta opção apenas se você planeja gerar milhares de coroutinas enquanto desejar ter uma pegada de baixa memória. Nem todos os ambientes têm um sistema operacional com suporte de memória virtual; portanto, esta opção está desativada por padrão.
Esta opção pode adicionar uma ordem de magnitude sobrecarga a mco_create() / mco_destroy() , porque solicitarão que o sistema operacional gerencie tabelas de página de memória virtual, se isso for um problema para você, personalize um alocador personalizado para suas próprias necessidades.
O seguinte pode ser definido para alterar o comportamento da biblioteca:
MCO_API - Qualificador de API público. O padrão é extern .MCO_MIN_STACK_SIZE - Tamanho mínimo da pilha ao criar uma coroutina. O padrão é 32768 (32kb).MCO_DEFAULT_STORAGE_SIZE - Tamanho do buffer de armazenamento de coroutine. O padrão é 1024.MCO_DEFAULT_STACK_SIZE - Tamanho da pilha padrão ao criar uma coroutina. O padrão é 57344 (56kb). Quando MCO_USE_VMEM_ALLOCATOR é verdadeiro, o padrão é 2040kb (quase 2MB).MCO_ALLOC - Função de alocação padrão. O padrão é calloc .MCO_DEALLOC - Função de desalocação padrão. O padrão é free .MCO_USE_VMEM_ALLOCATOR - Use alocador de memória virtual, melhorando a pegada de memória por coroutina.MCO_NO_DEFAULT_ALLOCATOR - Desative o alocador padrão usando MCO_ALLOC e MCO_DEALLOC .MCO_ZERO_MEMORY - Memória zero da pilha ao fazer armazenamento, destinado a ambientes coletados de lixo.MCO_DEBUG - Ativar modo de depuração, registrando qualquer erro de tempo de execução no stdout. Definido automaticamente, a menos que NDEBUG ou MCO_NO_DEBUG seja definido.MCO_NO_DEBUG - Desativar o modo de depuração.MCO_NO_MULTITHREAD - Desative o uso multithread. O multithread é suportado quando thread_local é suportado.MCO_USE_ASM - Uso de força da implementação do comutador de contexto de montagem.MCO_USE_UCONTEXT - Uso de força da implementação do Switch de contexto uContext.MCO_USE_FIBERS - Forçar o uso da implementação do comutador de contexto de fibras.MCO_USE_ASYNCIFY - Forçar o uso da implementação do comutador de contexto Binaryen ASyncify.MCO_USE_VALGRIND - Defina se você deseja executar com Valgrind para corrigir o acesso a erros de memória.A Biblioteca Coroutine foi comparada com X86_64 Counting Cycles para interruptor de contexto (acionado em currículo ou rendimento) e inicialização.
| CPU Arch | OS | Método | Interruptor de contexto | Inicializar | Não -inicializar |
|---|---|---|---|---|---|
| x86_64 | Linux | conjunto | 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 | conjunto | 33 ciclos | 74 ciclos | 14 ciclos |
Nota : Testado no Intel Core i7-8750H CPU @ 2.20GHz com coroutinas pré-alocadas.
Aqui está uma lista de todas as funções da biblioteca para referência 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. */A seguir, é apresentado um exemplo mais completo, gerando 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 Option para alocar milhares de coroutinas com baixa pegada de memória, isso inclui mudanças de ruptura na API do alocador.Sou um desenvolvedor de código aberto em tempo integral, qualquer quantidade da doação através do meu Github será apreciada e pode me incentivar para continuar apoiando esse e outros projetos de código aberto. Posso aceitar patrocínios únicos para pequenos recursos ou aprimoramentos menores alinhados com as metas do projeto, neste caso, entre em contato comigo.
Sua escolha de domínio público ou MIT Nenhuma atribuição, consulte o arquivo de licença.