Minicoro是单文件库,用于在C中使用不对称的Coroutines。API灵感来自Lua Coroutines,但考虑使用C。
该项目主要是成为Nelua编程语言的Coroutine后端。
图书馆大会实施受到Mike Pall的Lua Coco的启发。
大多数平台通过不同的方法支持:
| 平台 | 装配方法 | 后备方法 |
|---|---|---|
| 安卓 | 手臂/ARM64 | N/A。 |
| ios | 手臂/ARM64 | N/A。 |
| 视窗 | x86_64 | Windows纤维 |
| Linux | x86_64/i686 | UContext |
| Mac OS X | X86_64/ARM/ARM64 | UContext |
| WebAssembly | N/A。 | emscripten纤维 /二元异步 |
| 覆盆子pi | 手臂 | UContext |
| RISC-V | RV64/RV32 | UContext |
默认情况下,如果由编译器和CPU支持,则使用汇编方法,否则UContext或Fiber方法被用作后备。
组装方法非常有效,只需几个周期才能创建,恢复,屈服或破坏Coroutine。
mco_coro对象不是线程安全的,您应该在多线程应用程序中使用MUTEX对其进行操纵。thread_local限定符的C编译器进行编译。thread_local ,编译器可能会缓存线程局部变量指针,当Coroutine Switch线程时可能无效。-s ASYNCIFY=1进行编译。Coroutine代表独立的“绿色”执行线。但是,与多线程系统中的线程不同,Coroutine仅通过明确调用收益率函数来暂停其执行。
您可以通过调用mco_create创建Coroutine。它的唯一参数是一个mco_desc结构,其中描述了Coroutine。 mco_create功能仅创建一个新的Coroutine并返回手柄,它不会启动Coroutine。
您通过调用mco_resume执行Coroutine。调用简历函数时,Coroutine通过调用其身体功能开始执行。 Coroutine开始运行后,它将运行直至终止或屈服。
通过致电mco_yield来产生Coroutine。当Coroutine产生收益率时,即使在嵌套功能呼叫中发生的收益率发生(即,而不是在主函数中),相应的简历也会立即返回。下次您恢复相同的Coroutine时,它从屈服的地步开始执行。
要将持久值与Coroutine相关联,您可以选择在其创建中设置user_data ,然后使用mco_get_user_data检索。
要传递简历和收益率之间的值,您可以选择使用mco_push和mco_pop API,它们旨在使用LIFO样式缓冲区传递临时值。存储系统还可以用于在Coroutine创建或完成之前发送和接收初始值。
要使用Minicoro,请在一个.c文件中进行以下操作:
#define MINICORO_IMPL
#include "minicoro.h"您可以像其他任何标头一样在程序的其他部分中执行#include "minicoro.h" 。
以下简单示例演示了如何使用库:
#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 ;
}注意:如果您不想使用Minicoro分配系统,则应使用mco_desc.coro_size自己分配Coroutine对象并调用mco_init ,然后稍后销毁呼叫mco_uninit并进行处理。
您可以从任何地方产生当前的运行coroutine,而不必将mco_coro指针传递给此,只需使用mco_yield(mco_running())即可。
该库具有存储接口,以协助产量和简历之间传递数据。它的用法很简单,使用mco_push在mco_resume或mco_yield之前发送数据,然后在mco_resume或mco_yield之后使用mco_pop接收数据。注意不要将推动和弹出不匹配,否则这些功能将返回错误。
在滥用或系统错误的情况下,库在大多数API中返回错误代码,鼓励用户正确处理它们。
新的编译时间选项MCO_USE_VMEM_ALLOCATOR启用虚拟内存支持的分配器。
每个堆叠的Coroutine通常都必须为其完整的堆栈预留记忆,这通常会使数千个Coroutines的总记忆使用量非常高,例如,具有1000万个Coroutine的应用程序,具有56KB的堆栈的应用程序将消耗高达5GB的存储器,但是您的应用程序可能并不是每个Coroutine的全部堆栈使用情况。
由于这个问题,有些开发人员通常更喜欢无堆的旋ou旋场,而不是堆叠的珊瑚,而无堆的内存足迹很低,因此通常被认为更轻巧。但是,无匹配的局限性还有许多其他限制,例如您无法在其中运行不受限制的代码。
解决方案的一种补救措施是使堆叠式的Coroutines可生长,只能在真正需要时按需使用物理内存,并且有一种很好的方法可以在操作系统支持时依赖虚拟内存分配。
虚拟内存支持的分配器将为每个Coroutine堆栈保留虚拟内存,但尚未触发实际物理内存使用情况。尽管应用程序虚拟内存使用率很高,但物理内存使用率将很低,并且实际上按需增长(通常在Linux中的每个4KB块)。
虚拟内存后支持的分配器还将默认堆栈大小提高到约2MB,通常是Linux中额外线程的大小,因此您的Coroutines中有更多的空间,并且堆栈溢出的风险很低。
例如,用近2MB堆栈保留的空间与虚拟内存分配器分配了783MB的物理内存使用情况,即每个Coroutine约为8KB,但是虚拟内存使用率为98GB。
建议仅当您打算在想要具有低内存足迹的同时产生数千个Coroutines时才能启用此选项。并非所有环境都具有具有虚拟内存支持的OS,因此默认情况下该选项被禁用。
此选项可能会在mco_create() / mco_destroy()中添加一个数量级开销,因为它们会请求操作系统以管理虚拟内存页面表,如果这是您的问题,请自定义自定义分配器以满足您自己的需求。
可以定义以下以更改库行为:
MCO_API公共API预选赛。默认值是extern 。MCO_MIN_STACK_SIZE创建Coroutine时的最小堆栈尺寸。默认值为32768(32KB)。MCO_DEFAULT_STORAGE_SIZE Coroutine存储缓冲区的大小。默认值为1024。MCO_DEFAULT_STACK_SIZE创建Coroutine时默认堆栈大小。默认值为57344(56KB)。当MCO_USE_VMEM_ALLOCATOR为True时,默认值为2040KB(接近2MB)。MCO_ALLOC默认分配功能。默认值为calloc 。MCO_DEALLOC默认DealLocation函数。默认值是free 。MCO_USE_VMEM_ALLOCATOR使用虚拟内存支持的分配器,改善每个Coroutine的内存足迹。MCO_NO_DEFAULT_ALLOCATOR使用MCO_ALLOC和MCO_DEALLOC禁用默认分配器。MCO_ZERO_MEMORY堆栈存储时堆栈的零内存,用于垃圾收集的环境。MCO_DEBUG启用调试模式,记录任何运行时错误以符合Stdout。除非定义NDEBUG或MCO_NO_DEBUG否则自动定义。MCO_NO_DEBUG禁用调试模式。MCO_NO_MULTITHREAD禁用多线程用法。当支持thread_local时,支持多线程。MCO_USE_ASM汇编上下文开关实现的强制使用。MCO_USE_UCONTEXT强制使用UContext上下文开关实现。MCO_USE_FIBERS强制使用纤维上下文开关实现。MCO_USE_ASYNCIFY强制使用二进制异步上下文开关实现。MCO_USE_VALGRIND定义是否要使用Valgrind运行以修复访问内存错误。将Coroutine库用于X86_64计数CPU周期的上下文开关(以简历或产量触发)和初始化。
| CPU拱门 | 操作系统 | 方法 | 上下文开关 | 初始化 | 非初始化 |
|---|---|---|---|---|---|
| x86_64 | Linux | 集会 | 9个周期 | 31个周期 | 14个周期 |
| x86_64 | Linux | UContext | 352个周期 | 383个周期 | 14个周期 |
| x86_64 | 视窗 | 纤维 | 69个周期 | 10564周期 | 11167周期 |
| x86_64 | 视窗 | 集会 | 33个周期 | 74个周期 | 14个周期 |
注意:在Intel Core i7-8750h CPU @ 2.20GHz上进行了测试,并带有预分配的coroutines。
这是所有库函数的列表,供快速参考:
/* 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. */以下是一个更完整的示例,生成斐波那契数:
#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选项,用于分配数千个具有低内存足迹的Coroutines,这包括打破分配器API的变化。我是一名全日制开源开发人员,通过我的Github进行的任何捐赠都将受到赞赏,并可以鼓励我继续支持该和其他开源项目。我可能会接受一次性赞助,以供小型功能或与项目目标保持一致的次要增强功能,在这种情况下,请与我联系。
您选择的公共领域或MIT无归因,请参阅许可证文件。