GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||
GeezerVison is showing python.github.io — پشتیبانی افزونههای C API از نخبندی آزاد پشتیبانی افزونههای C API از نخبندی آزاد¶از نسخهی 3.13 به بعد، CPython از اجرا در پیکربندیای به نام قفل مفسر سراسری پشتیبانی میکند که در آن نخآزاد (GIL) غیرفعال است. این سند نحوهی سازگار کردن افزونههای C API برای پشتیبانی از نخآزاد را توضیح میدهد. شناسایی ساخت نخآزادی در C¶API زبان C در CPython ماکروی #ifdef Py_GIL_DISABLED /* code that only runs in the free-threaded build */ #endif نکته در ویندوز، این ماکرو بهطور خودکار تعریف نمیشود، بلکه باید هنگام ساخت به کامپایلر اعلام شود. برای تشخیص اینکه آیا این ماکرو در مفسر در حال اجرای فعلی تعریف شده است یا نه، میتوانید از تابع مقداردهی اولیه ماژول¶ماژولهای توسعهای باید بهصراحت اعلام کنند که از اجرا با GIL غیرفعال پشتیبانی میکنند؛ در غیر این صورت، ایمپورت کردن ماژول توسعهای باعث پرتاب یک هشدار میشود و GIL را در رانتایم فعال میکند. بسته به اینکه یک ماژول توسعه از مقداردهی اولیهی چندمرحلهای یا تکمرحلهای استفاده کند، دو روش برای نشان دادن پشتیبانی آن از اجرا با GIL غیرفعال وجود دارد. مقداردهی اولیهی چندمرحلهای¶افزونههایی که از مقداردهی اولیه چندمرحلهای (تابعونی مانند static struct PyModuleDef_Slot module_slots[] = { ... #if PY_VERSION_HEX >= 0x030D0000 {Py_mod_gil, Py_MOD_GIL_NOT_USED}, #endif {0, NULL} }; مقداردهی اولیهی تکمرحلهای¶افزونههایی که از مقداردهی اولیه تکمرحلهای قدیمی (یعنی static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, ... }; PyMODINIT_FUNC PyInit_mymodule(void) { PyObject *m = PyModule_Create(&moduledef); if (m == NULL) { return NULL; } #ifdef Py_GIL_DISABLED PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); #endif return m; } دستورالعملهای کلی API¶بیشتر C API نخایمن است، اما استثناهایی وجود دارد.
ایمنی نخ در ظرف¶ظرفهایی مانند
|
API ارجاع امانتی |
API ارجاع قوی |
|---|---|
هیچ (به PyDict_Next رجوع کنید) |
|
|
|
|
|
همهی APIهایی که ارجاعهای امانتی (borrowed references) را برمیگردانند، مشکلساز نیستند. برای مثال، PyTuple_GetItem() ایمن است، زیرا تاپلها تغییرناپذیر هستند. بهطور مشابه، همهی استفادههای APIهای بالا مشکلساز نیستند. برای مثال، PyDict_GetItem() اغلب برای تجزیهی دیکشنریهای آرگومان کلیدواژهای در فراخوانیهای تابع استفاده میشود؛ آن دیکشنریهای آرگومان کلیدواژهای عملاً خصوصی هستند (برای نخهای دیگر قابل دسترسی نیستند)، بنابراین استفاده از ارجاعهای امانتی در آن زمینه ایمن است.
برخی از این توابع در پایتون 3.13 افزوده شدهاند. شما میتوانید از بستهی pythoncapi-compat برای فراهم کردن پیادهسازی این توابع برای نسخههای قدیمیتر پایتون استفاده کنید.
C API مدیریت حافظهی پایتون، توابعی را در سه دامنهی تخصیص متفاوت فراهم میکند: "raw"، "mem" و "object". برای ایمنی نخها، ساخت free-threaded ایجاب میکند که تنها اشیای پایتون با استفاده از دامنهی object تخصیص داده شوند و همهی اشیای پایتون نیز با استفاده از آن دامنه تخصیص داده شوند. این با نسخههای پیشین پایتون تفاوت دارد؛ در آن نسخهها، این فقط یک بهترین روش بود و نه یک الزام سخت.
نکته
موارد استفاده از PyObject_Malloc() را در افزونه خود جستجو کنید و بررسی کنید که حافظه تخصیصیافته برای اشیای پایتون استفاده میشود. برای تخصیص بافرها به جای PyObject_Malloc() از PyMem_Malloc() استفاده کنید.
پایتون مجموعهای از توابع و ماکروها را برای مدیریت وضعیت نخ و GIL فراهم میکند، مانند:
این توابع همچنان باید در ساخت نخآزاد برای مدیریت وضعیت نخ حتی زمانی که GIL غیرفعال است، استفاده شوند. برای مثال، اگر نخی را خارج از پایتون ایجاد کنید، باید پیش از فراخوانی API پایتون، تابع PyThreadState_Ensure() را فراخوانی کنید تا مطمئن شوید که نخ دارای وضعیت نخ معتبری در پایتون است.
شما باید همچنان PyEval_SaveThread() یا Py_BEGIN_ALLOW_THREADS را در اطراف عملیات مسدودکننده، مانند I/O یا کسب قفل، فراخوانی کنید تا نخهای دیگر بتوانند زبالهروبی چرخهای را اجرا کنند.
ممکن است افزونه شما وضعیت داخلی داشته باشد که پیشتر توسط GIL محافظت میشد. ممکن است لازم باشد برای محافظت از این وضعیت، قفلگذاری اضافه کنید. رویکرد مورد استفاده به افزونه شما بستگی دارد، اما برخی الگوهای رایج عبارتاند از:
نهانگاهها: نهانگاههای سراسری منبع رایجی از وضعیت مشترک هستند. اگر نهانگاه برای کارایی حیاتی نیست، استفاده از یک قفل برای محافظت از نهانگاه یا غیرفعال کردن آن در ساخت نخآزاد را در نظر بگیرید.
وضعیت سراسری: ممکن است لازم باشد وضعیت سراسری با یک قفل محافظت شود یا به فضای ذخیرهسازی محلی نخ منتقل شود. C11 و C++11 برای فضای ذخیرهسازی محلی نخ، thread_local یا _Thread_local را ارائه میکنند.
در ساخت نخآزاد (free-threaded build)، CPython برای محافظت از دادههایی که در غیر این صورت توسط GIL محافظت میشدند، سازوکاری به نام «بخشهای بحرانی» فراهم میکند. اگرچه ممکن است نویسندگان افزونهها مستقیماً با پیادهسازی داخلی بخشهای بحرانی تعامل نداشته باشند، درک رفتار آنها هنگام استفاده از برخی از توابع C API یا مدیریت وضعیت مشترک در ساخت نخآزاد بسیار مهم است.
از نظر مفهومی، بخشهای بحرانی بهعنوان لایهای برای اجتناب از بنبست عمل میکنند که بر روی قفلهای متقابل ساده (mutex) ساخته شدهاند. هر نخ، پشتهای از بخشهای بحرانی فعال را نگه میدارد. هرگاه نخی نیاز به کسب قفلی مرتبط با یک بخش بحرانی داشته باشد (برای مثال، بهصورت ضمنی هنگام فراخوانی تابعی ایمن برای نخ از API C مانند PyDict_SetItem()، یا بهصورت صریح با استفاده از ماکروها)، تلاش میکند قفل متقابل زیربنایی را کسب کند.
APIهای اصلی برای استفاده از بخشهای بحرانی عبارتند از:
Py_BEGIN_CRITICAL_SECTION و Py_END_CRITICAL_SECTION - برای قفلکردن یک شیء واحد
Py_BEGIN_CRITICAL_SECTION2 و Py_END_CRITICAL_SECTION2 - برای قفل کردن همزمان دو شیء
این ماکروها باید بهصورت جفتهای متناظر استفاده شوند و در یک محدوده C یکسان ظاهر شوند، زیرا آنها یک محدوده محلی جدید ایجاد میکنند. این ماکروها در ساختهای non-free-threaded بدون عملیات هستند، بنابراین میتوان آنها را با اطمینان به کدی که نیاز به پشتیبانی از هر دو نوع ساخت دارد، اضافه کرد.
یک استفاده رایج از بخش بحرانی این است که هنگام دسترسی به یک ویژگی داخلی یک شیء، آن شیء را قفل کنید. برای مثال، اگر یک نوع توسعهای دارای یک فیلد شمارش داخلی باشد، میتوانید هنگام خواندن یا نوشتن آن فیلد از یک بخش بحرانی استفاده کنید:
// خواندن count، یک ارجاع جدید به مقدار داخلی count برمیگرداند PyObject *result; Py_BEGIN_CRITICAL_SECTION(obj); result = Py_NewRef(obj->count); Py_END_CRITICAL_SECTION(); return result; // نوشتن count، ارجاع new_count را مصرف میکند Py_BEGIN_CRITICAL_SECTION(obj); obj->count = new_count; Py_END_CRITICAL_SECTION();
برخلاف قفلهای سنتی، بخشهای بحرانی دسترسی انحصاری را در تمام مدت خود تضمین نمیکنند. اگر نخی در حالی که یک بخش بحرانی را در اختیار دارد مسدود شود (مثلاً با به دست آوردن قفلی دیگر یا انجام I/O)، آن بخش بحرانی بهطور موقت معلق میشود—همه قفلها آزاد میشوند—و سپس هنگامی که عملیات مسدودکننده کامل شود، از سر گرفته میشود.
این رفتار مشابه اتفاقی است که با GIL هنگامی رخ میدهد که یک نخ یک فراخوانی مسدودکننده انجام میدهد. تفاوتهای اصلی عبارتند از:
بخشهای بحرانی بهازای هر شیء عمل میکنند، نه بهصورت سراسری
بخشهای بحرانی در هر نخ از یک نظم پشتهای پیروی میکنند (ماکروهای «begin» و «end» این موضوع را اعمال میکنند، زیرا باید بهصورت جفت و در یک محدوده یکسان استفاده شوند)
بخشهای بحرانی بهطور خودکار قفلها را پیرامون عملیاتهای مسدودکنندهی احتمالی آزاد کرده و دوباره تصاحب میکنند
بخشهای بحرانی به دو روش در جلوگیری از بنبستها کمک میکنند:
اگر یک نخ تلاش کند قفلی را که از قبل در اختیار نخ دیگری است به دست آورد، ابتدا تمام بخشهای بحرانی فعال خود را معلق میکند و قفلهای آنها را بهطور موقت آزاد میکند
هنگامی که عملیات مسدودکننده کامل میشود، ابتدا تنها بالاترین بخش بحرانی دوباره به دست میآید
این بدان معناست که نمیتوانید برای قفل کردن چندین شیء بهطور همزمان، به بخشهای بحرانی تودرتو اتکا کنید، زیرا بخش بحرانی داخلی ممکن است بخشهای بحرانی بیرونی را معلق کند. در عوض، برای قفل کردن همزمان دو شیء از Py_BEGIN_CRITICAL_SECTION2 استفاده کنید.
توجه داشته باشید که قفلهای توصیفشده در بالا فقط قفلهای مبتنی بر PyMutex هستند. پیادهسازی بخش بحرانی از سایر سازوکارهای قفلسازی که ممکن است در حال استفاده باشند، مانند قفلهای متقابل POSIX، آگاه نیست و آنها را تحت تأثیر قرار نمیدهد. همچنین توجه داشته باشید که هرچند مسدود شدن در انتظار هر PyMutex باعث تعلیق بخشهای بحرانی میشود، فقط قفلهای متقابلی که بخشی از بخشهای بحرانی هستند آزاد میشوند. اگر PyMutex بدون یک بخش بحرانی استفاده شود، آزاد نخواهد شد و بنابراین از همان اجتناب از بنبست بهرهمند نمیشود.
بخشهای بحرانی ممکن است قفلهای خود را بهطور موقت آزاد کنند و به نخهای دیگر اجازه دهند دادههای محافظتشده را تغییر دهند. هنگام فرضسازی درباره وضعیت دادهها پس از عملیاتی که ممکن است مسدودکننده باشند، احتیاط کنید.
چون قفلها میتوانند بهطور موقت آزاد (معلق) شوند، ورود به یک بخش بحرانی، دسترسی انحصاری به منبع محافظتشده را در تمام مدت آن بخش تضمین نمیکند. اگر کد درون یک بخش بحرانی تابع دیگری را فراخوانی کند که مسدودکننده است (برای مثال، قفل دیگری را به دست میآورد یا ورودی/خروجی مسدودکننده انجام میدهد)، تمام قفلهایی که نخ از طریق بخشهای بحرانی در اختیار دارد آزاد خواهند شد. این شبیه به حالتی است که GIL میتواند در حین فراخوانیهای مسدودکننده آزاد شود.
تنها قفل(ها)ی مرتبط با آخرین بخش بحرانی واردشده (بالاترین) تضمین میشود که در هر لحظه نگهداشته شده باشند. قفلهای بخشهای بحرانی بیرونی و تودرتو ممکن است معلق شده باشند.
شما میتوانید با این APIها حداکثر ۲ شیء را بهطور همزمان قفل کنید. اگر نیاز دارید اشیاء بیشتری را قفل کنید، باید ساختار کد خود را بازآرایی کنید.
اگرچه بخشهای بحرانی در صورت تلاش برای قفل کردن دوبارهی یک شیء دچار بنبست نمیشوند، اما برای این مورد استفاده، کارآمدی کمتری نسبت به قفلهای بازورودپذیر (reentrant locks) دارند که برای همین منظور ساخته شدهاند.
هنگام استفاده از Py_BEGIN_CRITICAL_SECTION2، ترتیب شیءها بر صحت تأثیر نمیگذارد (پیادهسازی اجتناب از بنبست را مدیریت میکند)، اما بهتر است همیشه شیءها را با ترتیبی ثابت قفل کنید.
به یاد داشته باشید که ماکروهای بخش بحرانی عمدتاً برای محافظت از دسترسی به اشیای پایتون به کار میروند؛ این اشیاء ممکن است در عملیات داخلی CPython که مستعد سناریوهای بنبست توصیفشده در بالا هستند، دخیل باشند. برای محافظت از وضعیت کاملاً داخلی افزونه، ممکن است قفلهای متقابل استاندارد یا دیگر سازوکارهای همگامسازی مناسبتر باشند.
ob_mutex)¶در ساخت نخآزاد، هر شیء پایتون شامل یک فیلد ob_mutex از نوع PyMutex است. این قفل متقابل برای استفاده توسط API بخش بحرانی (Py_BEGIN_CRITICAL_SECTION / Py_END_CRITICAL_SECTION) رزرو شده است.
هشدار
ob_mutex را مستقیماً با PyMutex_Lock(&obj->ob_mutex) قفل نکنید. ترکیب فراخوانیهای مستقیم PyMutex_Lock با API بخش بحرانی روی همان قفل متقابل میتواند باعث ایجاد بنبست شود.
حتی اگر کد خودتان هرگز از بخشهای بحرانی روی یک نوع شیء خاص استفاده نکند، بخشهای داخلی سیپایتون ممکن است از API بخش بحرانی روی هر شیء پایتونی استفاده کنند.
اگر نوع افزونهی شما به قفل خودش نیاز دارد، یک فیلد مجزای PyMutex (یا یک اولیهی همگامسازی دیگر) به ساختار شیء خود اضافه کنید. PyMutex بسیار سبک است، بنابراین داشتن یک قفل اضافی هزینهی ناچیزی دارد.
افزونههای C API باید بهطور خاص برای ساخت نخآزادی ساخته شوند. wheelها، کتابخانههای مشترک و دودوییها با پسوند t مشخص میشوند.
pypa/manylinux از ساخت نخآزاد (free-threaded) با پسوند t، مانند python3.14t، پشتیبانی میکند.
pypa/cibuildwheel از ساخت wheelها برای نسخهی ساخت نخآزادی پایتون 3.14 و جدیدتر پشتیبانی میکند.
ساخت نخآزادی (free-threaded build) در حال حاضر از Limited C API یا ABI پایدار پشتیبانی نمیکند. اگر برای ساخت افزونه خود از setuptools استفاده میکنید و در حال حاضر py_limited_api=True را تنظیم کردهاید، میتوانید برای خارج شدن از API محدود هنگام ساخت با ساخت نخآزادی، از py_limited_api=not sysconfig.get_config_var("Py_GIL_DISABLED") استفاده کنید.
نکته
شما باید wheelهای جداگانهای را بهطور خاص برای ساخت نخآزاد بسازید. اگر در حال حاضر از ABI پایدار استفاده میکنید، میتوانید به ساخت یک wheel واحد برای چندین نسخهی پایتون غیرنخآزاد (non-free-threaded) ادامه دهید.
به دلیل محدودیتی در نصبکننده رسمی ویندوز، هنگام ساخت افزونهها از منبع، باید بهصورت دستی Py_GIL_DISABLED=1 را تعریف کنید.
همچنین ببینید
پورت ماژولهای توسعهای برای پشتیبانی از Free-Threading: یک راهنمای پورت نگهداریشده توسط کامیونیتی برای نویسندگان افزونه.