Minicoro هي مكتبة ملف واحد لاستخدام coroutines غير المتماثلة في C. و API مستوحاة من Lua Coroutines ولكن مع وضع C في الاعتبار.
يتم تطوير المشروع بشكل أساسي ليكون خلفية كوروتين للغة برمجة Nelua.
تطبيق مجموعة المكتبة مستوحى من Lua Coco بواسطة Mike Pall.
يتم دعم معظم المنصات من خلال طرق مختلفة:
| منصة | طريقة التجميع | طريقة العودة |
|---|---|---|
| Android | ARM/ARM64 | ن/أ |
| iOS | ARM/ARM64 | ن/أ |
| النوافذ | x86_64 | ألياف Windows |
| Linux | x86_64/i686 | ucontext |
| ماك OS X. | x86_64/ARM/ARM64 | ucontext |
| webassembly | ن/أ | الألياف emscripten / binaryen asyncify |
| التوت بي | ذراع | ucontext |
| RISC-V. | RV64/RV32 | ucontext |
يتم استخدام طريقة التجميع افتراضيًا إذا كان مدعومًا بواسطة برنامج التحويل البرمجي ووحدة المعالجة المركزية ، وإلا يتم استخدام طريقة uContext أو الألياف كاحتفال.
طريقة التجميع فعالة للغاية ، وتستغرق فقط بضع دورات لإنشاء أو استئناف أو إنتاج أو تدمير كوروتين.
mco_coro غير آمن مؤشر ترابط ، يجب عليك استخدام Mutex لمعالجة ذلك في تطبيقات Multithread.thread_local .thread_local داخل رمز coroutine ، قد يقوم برنامج التحويل البرمجي بتخزين مؤشرات المتغيرات المحلية لخيط الخيط والتي يمكن أن تكون غير صالحة عندما تكون مؤشرات ترابط مفتاح coroutine.-s ASYNCIFY=1 .يمثل coroutine مؤشر ترابط "أخضر" مستقل للتنفيذ. على عكس المواضيع في أنظمة Multithread ، ومع ذلك ، فإن coroutine يعلق فقط تنفيذها من خلال استدعاء وظيفة العائد بشكل صريح.
يمكنك إنشاء coroutine عن طريق الاتصال mco_create . حجةها الوحيدة هي بنية mco_desc مع وصف لـ Coroutine. تقوم وظيفة mco_create فقط بإنشاء coroutine جديد وإرجاع مقبض لها ، فهي لا تبدأ coroutine.
تقوم بتنفيذ coroutine عن طريق الاتصال mco_resume . عند استدعاء وظيفة السيرة الذاتية ، تبدأ Coroutine تنفيذها عن طريق استدعاء وظيفة الجسم. بعد بدء تشغيل Coroutine ، يتم تشغيله حتى ينتهي أو ينتج عنه.
ينتج coroutine عن طريق الاتصال mco_yield . عندما تعطي coroutine ، تعود السيرة الذاتية المقابلة على الفور ، حتى إذا حدث العائد داخل مكالمات الوظائف المتداخلة (أي ، ليس في الوظيفة الرئيسية). في المرة القادمة التي تستأنف فيها نفس كوروتين ، تستمر في تنفيذها من النقطة التي أسفرت عنها.
لربط قيمة ثابتة مع coroutine ، يمكنك ضبط user_data اختياريا على إنشائها واستردادها لاحقًا باستخدام mco_get_user_data .
لتمرير القيم بين السيرة الذاتية والعائد ، يمكنك اختياريا استخدام واجهات برمجة تطبيقات mco_push و mco_pop ، فهي تهدف إلى تمرير قيم مؤقتة باستخدام مخزن مؤقت نمط 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 ، يجب عليك تخصيص كائن Coroutine بنفسك باستخدام mco_desc.coro_size والاتصال بـ mco_init ، ثم لاحقًا لتدمير mco_uninit وتوصله.
يمكنك إنتاج Coroutine الحالي من أي مكان دون الحاجة إلى تمرير مؤشرات mco_coro حولها ، فقط استخدم mco_yield(mco_running()) .
تحتوي المكتبة على واجهة التخزين للمساعدة في تمرير البيانات بين العائد والسيرة الذاتية. الاستخدام واضح ومباشر ، استخدم mco_push لإرسال البيانات قبل mco_resume أو mco_yield ، ثم استخدم mco_pop لاحقًا بعد mco_resume أو mco_yield لتلقي البيانات. احرص على عدم عدم تطابق الدفع والبوب ، وإلا فإن هذه الوظائف سترجع خطأ.
رموز خطأ إرجاع المكتبة في معظم واجهة برمجة التطبيقات الخاصة بها في حالة سوء الاستخدام أو خطأ النظام ، يتم تشجيع المستخدم على التعامل معها بشكل صحيح.
يتيح خيار وقت الترجمة الجديد MCO_USE_VMEM_ALLOCATOR تخصيصًا مدعومًا ذاكرة افتراضية.
عادةً ما يتعين على كل coroutine مكاسلاً حجز الذاكرة للحصول على مكدسها الكامل ، وهذا عادة ما يجعل استخدام الذاكرة الكلي مرتفعًا جدًا عند تخصيص الآلاف من coroutines ، على سبيل المثال ، تطبيق مع 100 آلاف coroutine مع أكوام من 56 كيلو بايت سوف يستهلك ما يصل إلى 5 جيجابايت من الذاكرة ، ومع ذلك قد لا يكون تطبيقك ممتلئًا بالكامل لكل كوروتين.
غالبًا ما يفضل بعض المطورين coroutines بدون تكديس على coroutines المتراكمة بسبب هذه المشكلة ، فإن بصمة الذاكرة بدون تكديس منخفضة ، لذلك غالبًا ما تعتبر أكثر خفيفة الوزن. ومع ذلك ، فإن بدون قيود أخرى ، مثلما لا يمكنك تشغيل رمز غير مقيد بداخلها.
يتمثل أحد العلاج في الحل في جعل coroutines مكتوءات قابلة للتكاثر ، لاستخدام الذاكرة الفعلية فقط عند الطلب عند الحاجة إليها حقًا ، وهناك طريقة لطيفة للقيام بذلك بالاعتماد على تخصيص الذاكرة الافتراضية عند دعمها بواسطة نظام التشغيل.
سيحتفظ المخصص المدعوم من الذاكرة الافتراضية ذاكرة افتراضية في نظام التشغيل لكل مكدس Coroutine ، ولكن لا يؤدي إلى استخدام استخدام الذاكرة الفعلية الحقيقية حتى الآن. على الرغم من أن استخدام الذاكرة الافتراضية للتطبيق سيكون مرتفعًا ، إلا أن استخدام الذاكرة الفعلية سيكون منخفضًا وينمو فعليًا عند الطلب (عادةً ما يكون كل قطعة 4 كيلو بايت في Linux).
يرفع مخصصات الذاكرة المدعومة من الذاكرة الافتراضية أيضًا حجم المكدس الافتراضي إلى حوالي 2 ميغابايت ، وعادة ما يكون حجم الخيوط الإضافية في Linux ، بحيث يكون لديك مساحة أكبر في coroutines الخاص بك ويكون خطر الفائض في المكدس منخفضًا.
على سبيل المثال ، فإن تخصيص 100 ألف كوروتين مع مساحة مكدس بحوالي 2 ميجابايت مع مخصصة الذاكرة الافتراضية يستخدم 783 ميجابايت من استخدام الذاكرة الفعلية ، أي حوالي 8 كيلو بايت لكل كوروتين ، ومع ذلك سيكون استخدام الذاكرة الافتراضية عند 98 جيجابايت.
يوصى بتمكين هذا الخيار فقط إذا كنت تخطط لتفريغ الآلاف من coroutines مع الرغبة في الحصول على بصمة ذاكرة منخفضة. لا تحتوي جميع البيئات على نظام تشغيل مع دعم الذاكرة الظاهري ، وبالتالي يتم تعطيل هذا الخيار افتراضيًا.
قد يضيف هذا الخيار ترتيبًا من حيث الحجم إلى mco_create() / mco_destroy() ، لأنهم سيطلبون نظام التشغيل لإدارة جداول صفحة الذاكرة الافتراضية ، إذا كانت هذه مشكلة بالنسبة لك ، يرجى تخصيص مخصص مخصص لتلبية احتياجاتك الخاصة.
يمكن تعريف ما يلي لتغيير سلوك المكتبة:
MCO_API - مؤهلات API العامة. الافتراضي هو extern .MCO_MIN_STACK_SIZE - الحد الأدنى لحجم المكدس عند إنشاء coroutine. الافتراضي هو 32768 (32 كيلو بايت).MCO_DEFAULT_STORAGE_SIZE - حجم المخزن المؤقت لتخزين coroutine. الافتراضي هو 1024.MCO_DEFAULT_STACK_SIZE - حجم المكدس الافتراضي عند إنشاء coroutine. الافتراضي هو 57344 (56 كيلو بايت). عندما يكون MCO_USE_VMEM_ALLOCATOR صحيحًا ، فإن الافتراضي هو 2040 كيلو بايت (ما يقرب من 2 ميجابايت).MCO_ALLOC - وظيفة التخصيص الافتراضية. الافتراضي هو calloc .MCO_DEALLOC - دالة التخصيص الافتراضية. الافتراضي 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 - تعطيل استخدام Multithread. يتم دعم multithread عندما يتم دعم thread_local .MCO_USE_ASM - استخدام القوة لتنفيذ مفتاح سياق التجميع.MCO_USE_UCONTEXT - استخدام القوة لتنفيذ مفتاح سياق UContext.MCO_USE_FIBERS - استخدام القوة لتنفيذ مفتاح سياق الألياف.MCO_USE_ASYNCIFY - قوة استخدام تنفيذ مفتاح السياق Binaryen ASYNCIFY.MCO_USE_VALGRIND - حدد ما إذا كنت تريد التشغيل مع ValGrind لإصلاح الوصول إلى أخطاء الذاكرة.تم قياس مكتبة Coroutine لدورات CPU CPU X86_64 لمفتاح السياق (يتم تشغيله في السيرة الذاتية أو العائد) والتهيئة.
| قوس وحدة المعالجة المركزية | نظام التشغيل | طريقة | مفتاح السياق | تهيئة | غير مؤكد |
|---|---|---|---|---|---|
| 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 @ 2.20 جيجا هرتز مع 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 لتخصيص آلاف الكوروتين مع انخفاض البصمة للذاكرة ، وهذا يشمل تغيير التغييرات في واجهة برمجة تطبيقات مخصص.أنا مطور مفتوح المصدر بدوام كامل ، سيتم تقدير أي مبلغ من التبرع من خلال جيثب الخاص بي ويمكن أن يجلب لي التشجيع على الاستمرار في دعم هذا ومشاريع المصادر الأخرى. قد أقبل رعاية لمرة واحدة للميزات الصغيرة أو التحسينات البسيطة التي تتماشى مع أهداف المشروع ، في هذه الحالة اتصل بي.
اختيارك من المجال العام أو معهد ماساتشوستس للتكنولوجيا لا إسناد ، راجع ملف الترخيص.