ミニコロは、Cで非対称コルーチンを使用するための単一ファイルライブラリです。APIはLua Coroutinesに触発されていますが、Cを念頭に置いています。
このプロジェクトは、主にネルアプログラミング言語のコルーチンバックエンドとして開発されています。
図書館アセンブリの実装は、Mike PallのLua Cocoに触発されています。
ほとんどのプラットフォームは、さまざまな方法でサポートされています。
| プラットフォーム | アセンブリメソッド | フォールバック方法 |
|---|---|---|
| アンドロイド | ARM/ARM64 | n/a |
| iOS | ARM/ARM64 | n/a |
| Windows | x86_64 | Windows Fibers |
| Linux | x86_64/i686 | ucontext |
| Mac OS X | x86_64/arm/arm64 | ucontext |
| WebAssembly | n/a | Emscripten Fibers / Binaryen Asyncify |
| Raspberry Pi | アーム | ucontext |
| RISC-V | RV64/RV32 | ucontext |
アセンブリメソッドは、コンパイラとCPUによってサポートされている場合はデフォルトで使用されます。そうしないと、UContextまたはFiberメソッドがフォールバックとして使用されます。
アセンブリ方法は非常に効率的であり、コルーチンの作成、履歴書、降伏、または破壊には数サイクルかかります。
mco_coroオブジェクトはスレッドセーフではありません。マルチスレッドアプリケーションで操作するためにMutexを使用する必要があります。thread_local修飾子をサポートするCコンパイラでコンパイルする必要があります。thread_local使用を避けないでください。コンパイラは、Coroutineスイッチスレッドの場合に無効になる可能性のあるローカル変数ポインターをスレッドキャッシュする場合があります。-s ASYNCIFY=1でコンパイルする必要があります。コルーチンは、実行の独立した「グリーン」スレッドを表します。ただし、マルチスレッドシステムのスレッドとは異なり、コルーチンは、収量関数を明示的に呼び出すことにより、実行のみを停止します。
mco_createを呼び出すことにより、Coroutineを作成します。その唯一の議論は、Coroutineの説明があるmco_desc構造です。 mco_create関数は新しいCoroutineのみを作成し、ハンドルを返しますが、Coroutineを開始しません。
mco_resumeを呼び出してコルーチンを実行します。履歴書機能を呼び出すと、コルーチンはその身体関数を呼び出すことにより実行を開始します。 Coroutineが実行を開始した後、終了または降伏するまで実行されます。
mco_yieldを呼び出すことにより、コルーチンが降伏します。コルーチンの収量が発生すると、ネストされた関数呼び出し内(つまり、メイン関数ではなく)内で収量が発生した場合でも、対応する履歴書はすぐに戻ります。次回同じコルーチンを再開するとき、それが生み出した時点からその実行を続けます。
持続的な値をCoroutineに関連付けるには、オプションでuser_dataの作成を設定し、その後mco_get_user_dataで取得できます。
履歴書と利回りの間に値を渡すには、オプションでmco_pushとmco_pop APIを使用できます。これは、LIFOスタイルバッファーを使用して一時的な値を渡すことを目的としています。ストレージシステムは、Coroutine作成の初期値を送信および受信するためにも使用できます。
ミニコロを使用するには、1つの.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 ;
}注:ミニコロアロケーターシステムを使用したくない場合は、 mco_desc.coro_sizeを使用してCoroutineオブジェクトを自分で割り当ててmco_initを呼び出し、後でmco_uninitに電話して契約する必要があります。
mco_coroポインターmco_yield(mco_running())渡すことなく、どこからでも現在の実行コルーチンを生成できます。
ライブラリには、収量と履歴書の間のデータの渡すことを支援するストレージインターフェイスがあります。使用法は簡単です。MCO_RESUMEまたはmco_resume mco_yield前にmco_pushを使用してデータを送信し、その後mco_resumeまたはmco_yieldの後にmco_popを使用してデータを受信します。プッシュとポップを不一致にしないように注意してください。そうしないと、これらの関数はエラーを返します。
ライブラリは、誤用またはシステムエラーの場合にAPIのほとんどでエラーコードを返します。ユーザーは、それらを適切に処理することをお勧めします。
新しいコンパイル時間オプションMCO_USE_VMEM_ALLOCATOR 、仮想メモリバックアロケーターを有効にします。
通常、すべての積み重なったコルーチンは、完全なスタックのメモリを予約する必要があります。これにより、通常、数千のコルーチンを割り当てる場合、メモリの合計使用量が非常に高くなります。たとえば、56kbのスタックを持つ100,000のコルーチンを備えたアプリケーションは、5GBのメモリと同じように消費しますが、アプリケーションはすべてのコルーチンの完全なスタック使用量ではありません。
一部の開発者は、この問題のためにスタックフルコルタインよりもスタックレスコルーチンを好むことがよくあります。ただし、スタックレスには、制約のないコードを内部に実行できないように、他にも多くの制限があります。
解決策の救済策の1つは、積み重なったコルーチンを栽培可能にすること、実際に必要なときにオンデマンドで物理的なメモリを使用することであり、オペレーティングシステムでサポートされている場合に仮想メモリ割り当てに依存する良い方法があります。
仮想メモリバックされたAllocatorは、各CoroutineスタックのOSの仮想メモリを予約しますが、実際の物理メモリの使用量をまだトリガーしていません。アプリケーション仮想メモリの使用量は高くなりますが、物理メモリの使用量は低くなり、実際にはオンデマンドで成長します(通常、Linuxで4kbのチャンクごと)。
仮想メモリバックされたアロケーターは、デフォルトのスタックサイズを約2MB、通常はLinuxの追加スレッドのサイズに引き上げます。そのため、コルーチンにはより多くのスペースがあり、スタックオーバーフローのリスクは低くなります。
例として、仮想メモリアロケーターを使用して約2MBのスタック予約スペースを持つ1万のコルーチンを割り当てるには、783MBの物理メモリ使用量、つまりコルーチンあたり約8kbですが、仮想メモリの使用量は98GBになります。
このオプションを有効にすることをお勧めします。メモリフットプリントが低いことを望んでいる間に数千のコルーチンを発生させることを計画している場合にのみお勧めします。すべての環境に仮想メモリサポートを備えたOSがあるわけではないため、このオプションはデフォルトで無効になっています。
このオプションは、 mco_create() / mco_destroy()に1桁のオーバーヘッドを追加する場合があります。これは、仮想メモリページテーブルを管理するためにOSを要求するためです。これが問題である場合は、独自のニーズに合わせてカスタムアロケーターをカスタマイズしてください。
ライブラリの動作を変更するために、以下を定義できます。
MCO_APIパブリックAPI予選。デフォルトはexternです。MCO_MIN_STACK_SIZEコルーチンを作成するときの最小スタックサイズ。デフォルトは32768(32kb)です。MCO_DEFAULT_STORAGE_SIZE -Coroutineストレージバッファーのサイズ。デフォルトは1024です。MCO_DEFAULT_STACK_SIZEコルーチンを作成するときのデフォルトのスタックサイズ。デフォルトは57344(56kb)です。 MCO_USE_VMEM_ALLOCATORがtrueの場合、デフォルトは2040kb(ほぼ2MB)です。MCO_ALLOCデフォルトの割り当て関数。デフォルトはcallocです。MCO_DEALLOCデフォルトの取引ロケーション関数。デフォルトはfreeです。MCO_USE_VMEM_ALLOCATOR -Virtual Memory Backed Allocatorを使用して、Coroutineごとにメモリフットプリントを改善します。MCO_NO_DEFAULT_ALLOCATOR MCO_ALLOCとMCO_DEALLOCを使用してデフォルトのアロケーターを無効にします。MCO_ZERO_MEMORY -Garbageが収集した環境を対象としたストレージをポップするときのスタックのゼロメモリ。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 -Fibers Context Switchの実装の強制使用。MCO_USE_ASYNCIFY -Binaryenの強制使用コンテキストスイッチの実装。MCO_USE_VALGRIND -VALGRINDで実行してメモリエラーにアクセスすることを修正するかどうかを定義します。Coroutineライブラリは、コンテキストスイッチ(履歴書または収量でトリガー)および初期化のためにCPUサイクルをカウントするX86_64のベンチマークされました。
| CPUアーチ | OS | 方法 | コンテキストスイッチ | 初期化 | 無知 |
|---|---|---|---|---|---|
| x86_64 | Linux | 組み立て | 9サイクル | 31サイクル | 14サイクル |
| x86_64 | Linux | ucontext | 352サイクル | 383サイクル | 14サイクル |
| x86_64 | Windows | 繊維 | 69サイクル | 10564サイクル | 11167サイクル |
| x86_64 | Windows | 組み立て | 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オプションを紹介します。私はフルタイムのオープンソース開発者であり、GitHubからの寄付の量は高く評価され、これやその他のオープンソースプロジェクトをサポートし続けることを奨励することができます。プロジェクトの目標に沿った小さな機能またはマイナーな機能強化について、1回限りのスポンサーシップを受け入れることができます。この場合、私に連絡してください。
パブリックドメインまたはMITのいずれかの属性のいずれかを選択してください。ライセンスファイルを参照してください。