Minicoro adalah perpustakaan file tunggal untuk menggunakan coroutine asimetris di C. API ini terinspirasi oleh Lua coroutine tetapi dengan penggunaan C dalam pikiran.
Proyek ini sedang dikembangkan terutama untuk menjadi backend coroutine untuk bahasa pemrograman Nelua.
Implementasi perakitan perpustakaan terinspirasi oleh Lua Coco oleh Mike Pall.
Sebagian besar platform didukung melalui berbagai metode:
| Platform | Metode perakitan | Metode Fallback |
|---|---|---|
| Android | ARM/ARM64 | N/a |
| iOS | ARM/ARM64 | N/a |
| Windows | x86_64 | Serat jendela |
| Linux | x86_64/i686 | ucontext |
| Mac OS X. | x86_64/ARM/ARM64 | ucontext |
| WebAssembly | N/a | Emscripten Fibers / Binaryen Asyncify |
| Raspberry Pi | LENGAN | ucontext |
| Risc-v | RV64/RV32 | ucontext |
Metode perakitan digunakan secara default jika didukung oleh kompiler dan CPU, jika tidak, ucontext atau metode serat digunakan sebagai fallback.
Metode perakitan sangat efisien, hanya perlu beberapa siklus untuk membuat, melanjutkan, menghasilkan atau menghancurkan coroutine.
mco_coro tidak aman, Anda harus menggunakan mutex untuk memanipulasi dalam aplikasi multithread.thread_local .thread_local di dalam kode coroutine, kompiler dapat men -cache variabel variabel lokal yang bisa tidak valid ketika coroutine switch thread.-s ASYNCIFY=1 .Coroutine mewakili utas eksekusi "hijau" yang independen. Tidak seperti utas dalam sistem multithread, bagaimanapun, coroutine hanya menangguhkan eksekusi dengan secara eksplisit memanggil fungsi hasil.
Anda membuat coroutine dengan menelepon mco_create . Argumen tunggal adalah struktur mco_desc dengan deskripsi untuk coroutine. Fungsi mco_create hanya menciptakan coroutine baru dan mengembalikan pegangannya, ia tidak memulai coroutine.
Anda menjalankan coroutine dengan menelepon mco_resume . Saat memanggil fungsi resume, coroutine memulai eksekusi dengan menyebut fungsi tubuhnya. Setelah coroutine mulai berjalan, ia berjalan sampai berakhir atau menghasilkan.
Coroutine menghasilkan dengan menelepon mco_yield . Ketika coroutine menghasilkan, resume yang sesuai segera kembali, bahkan jika hasil terjadi di dalam panggilan fungsi bersarang (yaitu, bukan dalam fungsi utama). Lain kali Anda melanjutkan coroutine yang sama, ia melanjutkan eksekusi dari titik di mana ia menyerah.
Untuk mengaitkan nilai persisten dengan coroutine, Anda dapat secara opsional mengatur user_data pada pembuatannya dan kemudian mengambil dengan mco_get_user_data .
Untuk memberikan nilai antara resume dan hasil, Anda dapat secara opsional menggunakan mco_push dan API mco_pop , mereka dimaksudkan untuk melewati nilai sementara menggunakan buffer gaya LIFO. Sistem penyimpanan juga dapat digunakan untuk mengirim dan menerima nilai awal pada pembuatan coroutine atau sebelum selesai.
Untuk menggunakan minicoro, lakukan hal berikut dalam satu file .c:
#define MINICORO_IMPL
#include "minicoro.h" Anda dapat melakukan #include "minicoro.h" di bagian lain dari program seperti header lainnya.
Contoh sederhana berikut menunjukkan cara menggunakan perpustakaan:
#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 ;
} Catatan : Jika Anda tidak ingin menggunakan sistem alokasi minicoro, Anda harus mengalokasikan objek Coroutine sendiri menggunakan mco_desc.coro_size dan hubungi mco_init , kemudian untuk menghancurkan panggilan mco_uninit dan menanganinya.
Anda dapat menghasilkan coroutine yang berjalan saat ini dari mana saja tanpa harus lulus mointer mco_coro , untuk ini cukup gunakan mco_yield(mco_running()) .
Perpustakaan memiliki antarmuka penyimpanan untuk membantu memberikan data antara hasil dan resume. Penggunaannya mudah, gunakan mco_push untuk mengirim data sebelum mco_resume atau mco_yield , kemudian gunakan mco_pop setelah mco_resume atau mco_yield untuk menerima data. Berhati -hatilah untuk tidak membuat dorongan dan pop, jika tidak fungsi -fungsi ini akan mengembalikan kesalahan.
Kode kesalahan pengembalian perpustakaan di sebagian besar API jika terjadi kesalahan atau kesalahan sistem, pengguna didorong untuk menanganinya dengan benar.
Opsi Compile Time Baru MCO_USE_VMEM_ALLOCATOR Mengaktifkan alokasi yang didukung memori virtual.
Setiap stackful coroutine biasanya harus memesan memori untuk tumpukan penuhnya, ini biasanya membuat penggunaan total memori sangat tinggi ketika mengalokasikan ribuan coroutine, misalnya, aplikasi dengan 100 ribu coroutine dengan tumpukan 56kB akan dikonsumsi setinggi 5GB memori, namun aplikasi Anda mungkin tidak benar -benar penggunaan stack untuk setiap coroutine.
Beberapa pengembang sering lebih suka coroutines tanpa tumpukan daripada coroutine stackful karena masalah ini, jejak memori stackless rendah, oleh karena itu sering dianggap lebih ringan. Namun Stackless memiliki banyak batasan lain, seperti Anda tidak dapat menjalankan kode yang tidak dibatasi di dalamnya.
Salah satu obat untuk solusinya adalah membuat stackful coroutine dapat ditanam, untuk hanya menggunakan memori fisik sesuai permintaan ketika benar -benar dibutuhkan, dan ada cara yang bagus untuk melakukan ini mengandalkan alokasi memori virtual ketika didukung oleh sistem operasi.
Alokasi yang didukung memori virtual akan memesan memori virtual di OS untuk setiap tumpukan coroutine, tetapi belum memicu penggunaan memori fisik nyata. Sementara aplikasi penggunaan memori virtual akan tinggi, penggunaan memori fisik akan rendah dan benar -benar tumbuh sesuai permintaan (biasanya setiap potongan 4kB di Linux).
Alokasi yang didukung memori virtual juga meningkatkan ukuran tumpukan default menjadi sekitar 2MB, biasanya ukuran utas tambahan di Linux, sehingga Anda memiliki lebih banyak ruang di coroutine Anda dan risiko overflow stack rendah.
Sebagai contoh, mengalokasikan 100 ribu coroutines dengan hampir 2MB tumpukan ruang cadangan dengan alokasi memori virtual menggunakan 783MB penggunaan memori fisik, yaitu sekitar 8kb per coroutine, namun penggunaan memori virtual akan berada pada 98GB.
Disarankan untuk mengaktifkan opsi ini hanya jika Anda berencana untuk memunculkan ribuan coroutine sambil ingin memiliki jejak memori yang rendah. Tidak semua lingkungan memiliki OS dengan dukungan memori virtual, oleh karena itu opsi ini dinonaktifkan secara default.
Opsi ini dapat menambahkan urutan besarnya overhead ke mco_create() / mco_destroy() , karena mereka akan meminta OS untuk mengelola tabel halaman memori virtual, jika ini masalah bagi Anda, harap sesuaikan alokasi khusus untuk kebutuhan Anda sendiri.
Berikut ini dapat didefinisikan untuk mengubah perilaku perpustakaan:
MCO_API - Kualifikasi API Publik. Default adalah extern .MCO_MIN_STACK_SIZE - ukuran tumpukan minimum saat membuat coroutine. Default adalah 32768 (32kb).MCO_DEFAULT_STORAGE_SIZE - ukuran buffer penyimpanan coroutine. Default adalah 1024.MCO_DEFAULT_STACK_SIZE - ukuran tumpukan default saat membuat coroutine. Default adalah 57344 (56kb). Ketika MCO_USE_VMEM_ALLOCATOR benar, defaultnya adalah 2040kb (hampir 2MB).MCO_ALLOC - Fungsi alokasi default. Default adalah calloc .MCO_DEALLOC - fungsi deallocation default. Default free .MCO_USE_VMEM_ALLOCATOR - Gunakan alokasi yang didukung memori virtual, meningkatkan jejak memori per coroutine.MCO_NO_DEFAULT_ALLOCATOR - Nonaktifkan alokasi default menggunakan MCO_ALLOC dan MCO_DEALLOC .MCO_ZERO_MEMORY - nol memori tumpukan saat penyimpanan poping, dimaksudkan untuk lingkungan yang dikumpulkan.MCO_DEBUG - Aktifkan mode debug, logging kesalahan runtime ke stdout. Ditentukan secara otomatis kecuali NDEBUG atau MCO_NO_DEBUG didefinisikan.MCO_NO_DEBUG - Nonaktifkan mode debug.MCO_NO_MULTITHREAD - Nonaktifkan penggunaan multithread. Multithread didukung saat thread_local didukung.MCO_USE_ASM - Paksa penggunaan implementasi sakelar konteks perakitan.MCO_USE_UCONTEXT - Paksa penggunaan implementasi sakelar konteks ucontext.MCO_USE_FIBERS - Paksa penggunaan implementasi sakelar konteks serat.MCO_USE_ASYNCIFY - paksa penggunaan implementasi saklar konteks Binaryen Asyncify.MCO_USE_VALGRIND - Tentukan jika Anda ingin dijalankan dengan Valgrind untuk memperbaiki mengakses kesalahan memori.Perpustakaan Coroutine dibandingkan untuk siklus penghitungan CPU x86_64 untuk sakelar konteks (dipicu dalam resume atau hasil) dan inisialisasi.
| CPU Arch | Os | Metode | Sakelar konteks | Inisialisasi | Tidak diinisialisasi |
|---|---|---|---|---|---|
| x86_64 | Linux | perakitan | 9 siklus | 31 siklus | 14 siklus |
| x86_64 | Linux | ucontext | 352 siklus | 383 siklus | 14 siklus |
| x86_64 | Windows | serat | 69 siklus | 10564 siklus | 11167 siklus |
| x86_64 | Windows | perakitan | 33 siklus | 74 siklus | 14 siklus |
Catatan : Diuji pada Intel Core i7-8750h CPU @ 2.20GHz dengan coroutine yang dialokasikan sebelumnya.
Berikut adalah daftar semua fungsi perpustakaan untuk referensi cepat:
/* 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. */Berikut ini adalah contoh yang lebih lengkap, menghasilkan angka 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 untuk mengalokasikan ribuan coroutine dengan jejak memori rendah, ini termasuk perubahan perubahan dalam API Allocator.Saya seorang pengembang open source penuh waktu, jumlah donasi apa pun melalui github saya akan dihargai dan dapat memberi saya dorongan untuk terus mendukung ini dan proyek open source lainnya. Saya dapat menerima sponsor satu kali untuk fitur-fitur kecil atau peningkatan kecil yang selaras dengan tujuan proyek, dalam hal ini hubungi saya.
Pilihan domain publik Anda atau atribusi MIT tanpa, lihat file lisensi.