Minicoro는 C에서 비대칭 코 루틴을 사용하기위한 단일 파일 라이브러리입니다.
이 프로젝트는 주로 NELUA 프로그래밍 언어의 코 루틴 백엔드로 개발되었습니다.
도서관 어셈블리 구현은 Mike Pall의 Lua Coco에서 영감을 받았습니다.
대부분의 플랫폼은 다양한 방법을 통해 지원됩니다.
| 플랫폼 | 조립 방법 | 폴백 방법 |
|---|---|---|
| 기계적 인조 인간 | 팔/암 64 | N/A |
| iOS | 팔/암 64 | N/A |
| 창 | x86_64 | 창 섬유 |
| 리눅스 | x86_64/i686 | ucontext |
| Mac OS x | x86_64/arm/arm64 | ucontext |
| webassembly | N/A | Emscripten 섬유 / Binaryen Asyncify |
| 라즈베리 파이 | 팔 | ucontext |
| RISC-V | RV64/RV32 | ucontext |
어셈블리 방법은 컴파일러 및 CPU에서 지원하는 경우 기본적으로 사용되며, 그렇지 않으면 ucontext 또는 파이버 방법이 폴백으로 사용됩니다.
어셈블리 방법은 매우 효율적이며 코 루틴을 생성, 재개, 수율 또는 파괴하는 데 몇주기가 걸립니다.
mco_coro 객체는 스레드 안전하지 않으므로 MUTEX를 사용하여 MultithRead 응용 프로그램에서 조작해야합니다.thread_local Qualifier를 지원하는 C 컴파일러를 컴파일해야합니다.thread_local 사용하지 않으면 컴파일러는 Corootine이 스레드를 전환 할 때 유효하지 않은 로컬 변수 포인터를 캐시 할 수 있습니다.-s ASYNCIFY=1 으로 컴파일해야합니다.코 루틴은 실행의 독립적 인 "녹색"스레드를 나타냅니다. 그러나 멀티 스레드 시스템의 스레드와 달리, 코 루틴은 항복 함수를 명시 적으로 호출하여 실행을 일시 중단합니다.
mco_create 에 전화하여 코 루틴을 만듭니다. 유일한 인수는 코 루틴에 대한 설명이있는 mco_desc 구조입니다. mco_create 함수는 새로운 코 루틴 만 생성하고 핸들을 반환하면 코 루틴을 시작하지 않습니다.
mco_resume 에 전화하여 코 루틴을 실행합니다. 이력서 기능을 호출 할 때 Coroutine은 신체 기능을 호출하여 실행을 시작합니다. 코 루틴이 실행되기 시작한 후에는 종료되거나 생산 될 때까지 실행됩니다.
코 루틴은 mco_yield 호출하여 생산됩니다. 코 루틴이 생성되면 중첩 된 기능 호출 내에서 수율이 발생하더라도 해당 이력서가 즉시 반환됩니다 (즉, 주 기능이 아님). 다음에 동일한 코 루틴을 재개 할 때, 그것은 양보 된 지점에서 계속 실행됩니다.
지속적인 값을 Coroutine과 연관 시키려면 선택적으로 user_data 생성에서 설정하고 나중에 mco_get_user_data 로 검색 할 수 있습니다.
이력서와 수율 사이의 값을 전달하려면 선택적으로 mco_push 및 mco_pop API를 사용할 수 있으며 LIFO 스타일 버퍼를 사용하여 임시 값을 전달하도록 의도됩니다. 스토리지 시스템은 또한 코 루틴 생성에서 또는 완료되기 전에 초기 값을 보내고받는 데 사용될 수 있습니다.
미니 코로를 사용하려면 하나의 .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 Allocator 시스템을 사용하지 않으려면 mco_desc.coro_size 사용하여 Coroutine 객체를 직접 할당하고 mco_init 전화 한 다음 나중에 mco_uninit 호출을 파괴하고 거래 할 수 있습니다.
mco_coro 포인터를 통과하지 않고도 현재의 현재 실행중인 코 루틴을 산출 할 수 있습니다 mco_yield(mco_running())
라이브러리에는 수율과 이력서 사이의 데이터를 전달하는 데 도움이되는 스토리지 인터페이스가 있습니다. 사용량은 간단합니다. mco_push 사용하여 mco_resume 또는 mco_yield 전에 데이터를 보내고 나중에 mco_resume 또는 mco_yield 다음에 mco_pop 사용하여 데이터를 수신하십시오. 푸시 앤 팝을 일치하지 않도록주의하십시오. 그렇지 않으면 이러한 기능이 오류를 반환합니다.
오용 또는 시스템 오류의 경우 대부분의 API의 라이브러리 반환 오류 코드는 사용자가 올바르게 처리하도록 권장됩니다.
새로운 컴파일 타임 옵션 MCO_USE_VMEM_ALLOCATOR 가상 메모리 백업 할당자를 활성화합니다.
모든 스택이 많은 코 루틴은 일반적으로 전체 스택을 위해 메모리를 예약해야합니다. 이것은 일반적으로 수천 개의 코 루틴을 할당 할 때 총 메모리 사용량을 매우 높게 만듭니다. 예를 들어, 56KB 스택이있는 10 만 개의 코 루틴이있는 응용 프로그램은 5GB의 메모리만큼 높은 메모리를 소비 할 수 있지만, 모든 코 루틴에 대한 완전한 스택 사용량은 실제로 완전한 스택 사용량을 사용하지 않을 수 있습니다.
일부 개발자는 종종이 문제로 인해 스택이없는 코 루틴보다 스택이없는 코 루틴을 선호합니다. 스택리스 메모리 풋 프린트는 낮으므로 종종 더 가벼운 것으로 간주됩니다. 그러나 Stackless는 구속되지 않은 코드를 실행할 수없는 것처럼 다른 많은 제한 사항이 있습니다.
솔루션에 대한 한 가지 구제책은 스택 형 코 루틴을 재배 가능하게 만들고 실제로 필요할 때 주문시 물리적 메모리 만 사용하는 것입니다. 운영 체제에서 지원할 때 가상 메모리 할당에 의존하는 좋은 방법이 있습니다.
가상 메모리 백업 할당자는 각 코 루틴 스택에 대한 OS의 가상 메모리를 예약하지만 실제 물리적 메모리 사용을 아직 트리거하지는 않습니다. 애플리케이션 가상 메모리 사용량은 높지만 물리적 메모리 사용은 낮아지고 실제로 주문형으로 증가합니다 (일반적으로 Linux의 4KB 덩어리마다).
가상 메모리 뒷받침 할당자는 기본 스택 크기를 약 2MB, 일반적으로 Linux의 추가 스레드 크기로 올리므로 코 루틴에 더 많은 공간이 있고 스택 오버플로의 위험이 낮습니다.
예를 들어, 가상 메모리 Allocator와 함께 거의 2MB 스택 예약 공간을 갖춘 10 만 개의 코 루틴을 할당하면 783MB의 물리적 메모리 사용량을 사용하지만, 이는 코 루틴 당 약 8KB이지만 가상 메모리 사용량은 98GB입니다.
메모리 풋 프린트가 낮은 상태에서 수천 개의 코 루틴을 생성하려는 경우에만이 옵션을 활성화하는 것이 좋습니다. 모든 환경에 가상 메모리 지원이있는 OS가있는 것은 아니므 로이 옵션은 기본적으로 비활성화됩니다.
이 옵션은 mco_create() / mco_destroy() 에 크기의 오버 헤드를 추가 할 수 있습니다. 왜냐하면 OS는 가상 메모리 페이지 테이블을 관리하도록 요청하기 때문입니다. 문제가되는 경우 자신의 요구에 맞는 사용자 정의 할당자를 사용자 정의하십시오.
다음은 라이브러리 동작을 변경하기 위해 정의 될 수 있습니다.
MCO_API 공개 API 예선. 기본값은 extern 입니다.MCO_MIN_STACK_SIZE 코 루틴을 만들 때 최소 스택 크기. 기본값은 32768 (32KB)입니다.MCO_DEFAULT_STORAGE_SIZE 코 루틴 스토리지 버퍼의 크기. 기본값은 1024입니다.MCO_DEFAULT_STACK_SIZE 기본 스택 크기는 코 루틴을 만들 때입니다. 기본값은 57344 (56KB)입니다. MCO_USE_VMEM_ALLOCATOR 참이면 기본값은 2040kb (거의 2MB)입니다.MCO_ALLOC 기본 할당 함수. 기본값은 calloc 입니다.MCO_DEALLOC 기본 거래 기능. 기본값은 free 입니다.MCO_USE_VMEM_ALLOCATOR 가상 메모리 백업 할당자를 사용하여 코 루틴 당 메모리 풋 프린트를 개선합니다.MCO_NO_DEFAULT_ALLOCATOR MCO_ALLOC 및 MCO_DEALLOC 사용하여 기본 할당자를 비활성화합니다.MCO_ZERO_MEMORY 스토리지가 튀어 나올 때 스택 메모리 제로 메모리는 쓰레기 수집 환경을 대상으로합니다.MCO_DEBUG 디버그 모드를 활성화하고 런타임 오류를 stdout에 기록합니다. NDEBUG 또는 MCO_NO_DEBUG 정의되지 않으면 자동으로 정의됩니다.MCO_NO_DEBUG 디버그 모드 비활성화.MCO_NO_MULTITHREAD 멀티 스레드 사용을 비활성화합니다. Multithread는 thread_local 지원되면 지원됩니다.MCO_USE_ASM 어셈블리 컨텍스트 스위치 구현의 힘 사용.MCO_USE_UCONTEXT UContext 컨텍스트 스위치 구현의 힘 사용.MCO_USE_FIBERS 섬유 컨텍스트 스위치 구현의 힘 사용.MCO_USE_ASYNCIFY Binaryen Asyncify Context Switch 구현의 힘 사용.MCO_USE_VALGRIND 메모리 오류 액세스를 수정하기 위해 Valgrind로 실행할 것인지 정의하십시오.코 루틴 라이브러리는 컨텍스트 스위치 (이력서 또는 수율로 트리거) 및 초기화에 대한 X86_64 카운팅 CPU 사이클을 위해 벤치마킹되었습니다.
| CPU 아치 | OS | 방법 | 컨텍스트 스위치 | 초기화 | 비 초기화 |
|---|---|---|---|---|---|
| x86_64 | 리눅스 | 집회 | 9 사이클 | 31주기 | 14 사이클 |
| x86_64 | 리눅스 | ucontext | 352 사이클 | 383 사이클 | 14 사이클 |
| x86_64 | 창 | 섬유 | 69 사이클 | 10564 사이클 | 11167 사이클 |
| x86_64 | 창 | 집회 | 33 사이클 | 74 사이클 | 14 사이클 |
참고 : 사전 할당 된 코 루틴으로 Intel Core i7-8750H CPU @ 2.20GHz에서 테스트되었습니다.
다음은 빠른 참조를위한 모든 라이브러리 기능 목록입니다.
/* 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. */다음은 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 옵션을 소개합니다. 여기에는 Allocator API의 변화가 나빠집니다.저는 풀 타임 오픈 소스 개발자입니다. GitHub를 통한 기부금은 모두 감사하게 될 것이며이 오픈 소스 프로젝트 및 기타 오픈 소스 프로젝트를 계속 지원하도록 격려 할 수 있습니다. 프로젝트 목표와 일치하는 작은 기능 또는 사소한 개선 사항에 대한 일회성 후원을 수락 할 수 있습니다.이 경우 저에게 연락하십시오.
공개 도메인 또는 MIT가 귀속이 없음을 선택하십시오. 라이센스 파일을 참조하십시오.