پشتیبانی پایتون از نخ‌بندی آزاد

از نسخه‌ی 3.13، CPython از ساختی از پایتون به نام نخ‌بندی آزاد (free threading) پشتیبانی می‌کند که در آن قفل سراسری مفسر (global interpreter lock، GIL) غیرفعال است. اجرای نخ‌بندی آزاد امکان بهره‌برداری کامل از توان پردازشی موجود را با اجرای موازی نخ‌ها بر روی هسته‌های CPU موجود فراهم می‌کند. اگرچه همه‌ی نرم‌افزارها به‌طور خودکار از آن بهره‌مند نخواهند شد، برنامه‌هایی که با در نظر گرفتن نخ‌بندی طراحی شده‌اند، روی سخت‌افزار چندهسته‌ای سریع‌تر اجرا خواهند شد.

برخی بسته‌های شخص ثالث، به‌ویژه آن‌هایی که یک extension module دارند، ممکن است برای استفاده در یک ساخت نخ‌آزادی (free-threaded build) آماده نباشند، و GIL را دوباره فعال خواهند کرد.

این سند پیامدهای نخ‌بندی آزاد برای کد پایتون را توضیح می‌دهد. برای اطلاع از نحوه نوشتن افزونه‌های C که از ساخت نخ‌آزاد (free-threaded build) پشتیبانی می‌کنند، پشتیبانی افزونه‌های C API از نخ‌بندی آزاد را ببینید.

همچنین ملاحظه نمائید

PEP 703 – اختیاری کردن قفل سراسری مفسر در CPython، برای توصیفی کلی از پایتون نخ‌آزاد (free-threaded Python).

نصب

از پایتون 3.13 به بعد، نصب‌کننده‌های رسمی macOS و Windows به‌صورت اختیاری از نصب دودویی‌های نخ‌آزاد پایتون پشتیبانی می‌کنند. این نصب‌کننده‌ها در https://www.python.org/downloads/ در دسترس هستند.

برای اطلاعات درباره‌ی سایر پلتفرم‌ها، نصب پایتون نخ‌آزاد را ببینید، یک راهنمای نصب نگهداری‌شده توسط کامیونیتی برای نصب پایتون نخ‌آزاد.

هنگام ساخت CPython از منبع، برای ساخت یک مفسر پایتون نخ‌آزاد باید از گزینه‌ی --disable-gil در configure استفاده شود.

شناسایی پایتون نخ‌آزاد

برای بررسی این‌که آیا مفسر فعلی از نخ‌آزاد پشتیبانی می‌کند، python -VV و sys.version شامل "free-threading build" هستند. می‌توانید از تابع جدید sys._is_gil_enabled() برای بررسی این‌که آیا قفل سراسری مفسر (GIL) در فرایند در حال اجرا واقعاً غیرفعال است، استفاده کنید.

می‌توان از متغیر پیکربندی sysconfig.get_config_var("Py_GIL_DISABLED") برای تعیین اینکه آیا ساخت از نخ‌بندی آزاد پشتیبانی می‌کند، استفاده کرد. اگر این متغیر روی 1 تنظیم شده باشد، ساخت از نخ‌بندی آزاد پشتیبانی می‌کند. این سازوکار توصیه‌شده برای تصمیم‌های مربوط به پیکربندی ساخت است.

قفل سراسری مفسر در پایتون نخ‌آزاد

ساخت‌های نخ‌آزادی CPython از اجرای اختیاری با GIL فعال در ران‌تایم، با استفاده از متغیر محیطی PYTHON_GIL یا گزینه‌ی خط فرمان -X gil پشتیبانی می‌کنند.

ممکن است GIL همچنین به‌صورت خودکار هنگام ایمپورت کردن یک ماژول توسعه‌ی C-API که به‌صراحت به‌عنوان پشتیبانی‌کننده از نخ‌بندی آزاد علامت‌گذاری نشده باشد، فعال شود. در این حالت هشداری چاپ خواهد شد.

علاوه بر مستندات هر بسته، وب‌سایت‌های زیر وضعیت پشتیبانی بسته‌های محبوب از نخ‌بندی آزاد را پیگیری می‌کنند:

ایمنی نخ

ساخت نخ‌آزاد CPython هدف دارد در سطح پایتون، رفتار ایمنی نخی مشابهی با ساخت پیش‌فرض دارای GIL فعال فراهم کند. انواع توکار مانند dict، list و set از قفل‌های داخلی برای محافظت در برابر تغییرات همزمان به روش‌هایی استفاده می‌کنند که رفتاری مشابه GIL دارند. با این حال، پایتون از نظر تاریخی رفتار مشخصی را برای تغییرات همزمان روی این انواع توکار تضمین نکرده است، بنابراین این موضوع باید به‌عنوان توصیفی از پیاده‌سازی کنونی در نظر گرفته شود، نه تضمینی برای رفتار کنونی یا آینده.

توجه

توصیه می‌شود در صورت امکان به‌جای تکیه بر قفل‌های داخلی انواع توکار، از threading.Lock یا دیگر اولیه‌های همگام‌سازی (synchronization primitives) استفاده کنید.

محدودیت‌های شناخته‌شده

این بخش محدودیت‌های شناخته‌شده‌ی ساخت CPython با نخ‌های آزادرا شرح می‌دهد.

نامیراسازی

در ساخت free-threaded، برخی اشیاء نامیرا هستند. اشیاء نامیرا آزاد نمی‌شوند و شمارش ارجاع آن‌ها هرگز تغییر نمی‌کند. این کار برای اجتناب از رقابت بر سر شمارش ارجاع انجام می‌شود که مانع از مقیاس‌پذیری کارآمد چندنخی می‌شود.

از زمان انتشار نسخه‌ی 3.14، نامیرا شدن محدود است به:

  • مقادیر ثابت کد: مقادیر لفظی عددی، رشته‌ای و تاپلی تشکیل‌شده از ثابت‌های دیگر.

  • رشته‌های درونی‌سازی‌شده توسط sys.intern().

اشیای فریم

دسترسی به frame.f_locals از یک شیء فریم در صورتی که آن فریم در حال حاضر در نخ دیگری در حال اجرا است، ایمن نیست و انجام این کار ممکن است باعث از کار افتادن مفسر شود.

پیمایش‌گرها

دسترسی همزمان به یک شیء پیمایش‌گر یکسان از طریق چندین نخ، عموماً ایمن در برابر نخ (thread-safe) نیست و نخ‌ها ممکن است عناصر تکراری یا ازدست‌رفته را مشاهده کنند.

عملکرد تک‌نخی

ساخت نخ‌آزاد (free-threaded build) در مقایسه با ساخت پیش‌فرض با GIL فعال، هنگام اجرای کد پایتون سربار اضافی دارد. میزان سربار به بار کاری و سخت‌افزار بستگی دارد. در بدنه بنچمارک pyperformance، میانگین سربار از حدود ۱٪ در macOS aarch64 تا ۸٪ در سیستم‌های لینوکس x86-64 متغیر است.

تغییرات رفتاری

این بخش تغییرات رفتاری CPython را در ساخت نخ‌آزاد (free-threaded build) توصیف می‌کند.

متغیرهای زمینه

در ساخت نخ‌آزاد، پرچم thread_inherit_context به‌طور پیش‌فرض روی true تنظیم شده است که باعث می‌شود نخ‌های ایجادشده با threading.Thread با یک کپی از Context() متعلق به فراخواننده‌ی start() آغاز شوند. در ساخت پیش‌فرض با GIL فعال (GIL-enabled)، این پرچم به‌طور پیش‌فرض false است، بنابراین نخ‌ها با یک Context() خالی آغاز می‌شوند.

فیلترهای هشدار

در ساخت نخ‌آزادی، پرچم context_aware_warnings به‌صورت پیش‌فرض روی درست تنظیم شده است. در ساخت پیش‌فرض با GIL فعال (GIL-enabled)، این پرچم به‌طور پیش‌فرض نادرست است. اگر پرچم درست باشد، مدیر زمینه warnings.catch_warnings از یک متغیر زمینه برای فیلترهای هشدار استفاده می‌کند. اگر پرچم نادرست باشد، catch_warnings فهرست فیلترهای سراسری را تغییر می‌دهد، که ایمن برای نخ‌ها (thread-safe) نیست. برای جزئیات بیشتر، ماژول warnings را ببینید.

افزایش مصرف حافظه

ساخت با نخ‌های آزادبه‌طور معمول در مقایسه با ساخت پیش‌فرض از حافظه‌ی بیشتری استفاده می‌کند. دلایل متعددی برای این موضوع وجود دارد که عمدتاً به‌دلیل تصمیم‌های طراحی است.

همه‌ی رشته‌های درونی‌سازی‌شده نامیرا هستند

در نسخه‌های مدرن پایتون (از نسخه‌ی 2.3)، اینترن کردن (interning) یک رشته (برای نمونه با sys.intern()) باعث نامیرا شدن آن نمی‌شود. در عوض، اگر آخرین ارجاع به آن رشته از بین برود، آن رشته از جدول رشته‌های درونی‌سازی‌شده (interned string table) حذف خواهد شد. این موضوع در مورد ساخته‌ی نخ‌آزادی (free-threaded build) صدق نمی‌کند و هر رشته‌ی درونی‌سازی‌شده‌ای نامیرا خواهد شد و تا خاموش شدن مفسر باقی خواهد ماند.

اشیای غیر GC سرآیند شیء بزرگ‌تری دارند

ساخت نخ‌آزاد از ساختار PyObject متفاوتی استفاده می‌کند. به جای این که اطلاعات مربوط به زباله‌روبی، مانند ساخت پیش‌فرض، پیش از ساختار PyObject تخصیص داده شود، این اطلاعات بخشی از سرآیند معمولی شیء است. برای نمونه، در سکوی AMD64، None در ساخت نخ‌آزاد از ۳۲ بایت استفاده می‌کند، در مقابل ۱۶ بایت برای ساخت پیش‌فرض. اشیای تحت مدیریت زباله‌روبی (مانند دیکشنری‌ها و فهرست‌ها) در هر دو ساخت اندازه یکسانی دارند، زیرا ساخت نخ‌آزاد از فضای اضافی برای اطلاعات مربوط به زباله‌روبی استفاده نمی‌کند.

QSBR می‌تواند آزادسازی حافظه را به تأخیر بیندازد

برای پیاده‌سازی امن ساختارهای داده بدون قفل (lock-free)، از یک طرح بازیابی امن حافظه (SMR) استفاده می‌شود که با نام بازیابی مبتنی بر حالت سکون (QSBR) شناخته می‌شود. این بدان معناست که حافظه پشتیبان ساختارهای داده‌ای که دسترسی بدون قفل را ممکن می‌سازند، از QSBR استفاده می‌کند؛ این طرح عملیات آزادسازی را به تعویق می‌اندازد، نه اینکه حافظه را بلافاصله آزاد کند. دو نمونه از این ساختارهای داده، شیء فهرست و شیء کلیدهای دیکشنری هستند. برای جزئیات بیشتر درباره نحوه پیاده‌سازی QSBR، پرونده InternalDocs/qsbr.md را در درخت منبع CPython ببینید. اجرای gc.collect() باید موجب شود تمام حافظه‌ای که توسط QSBR نگه‌داری می‌شود، به‌طور واقعی آزاد شود. توجه داشته باشید که حتی وقتی QSBR حافظه را آزاد می‌کند، تخصیص‌دهنده حافظه زیرین ممکن است بلافاصله آن حافظه را به سیستم‌عامل برنگرداند و بنابراین اندازه مجموعه مقیم (RSS) فرایند ممکن است کاهش نیابد.

تخصیص‌دهنده‌ی mimalloc در مقابل pymalloc

ساخت پیش‌فرض معمولاً از تخصیص‌دهنده‌ی حافظه‌ی "pymalloc" برای تخصیص‌های کوچک (۵۱۲ بایت یا کوچک‌تر) استفاده می‌کند. ساخت نخ‌آزاد از pymalloc استفاده نمی‌کند و همه‌ی اشیای پایتون را با استفاده از تخصیص‌دهنده‌ی "mimalloc" تخصیص می‌دهد. تخصیص‌دهنده‌ی pymalloc ویژگی‌های زیر را دارد که به پایین نگه‌داشتن مصرف حافظه کمک می‌کنند: سربار کم به ازای هر بلوک تخصیص‌داده‌شده، جلوگیری مؤثر از تکه‌تکه شدن حافظه، و بازگرداندن سریع حافظه‌ی آزاد به سیستم‌عامل. تخصیص‌دهنده‌ی mimalloc نیز در این زمینه‌ها عملکرد بسیار خوبی دارد، اما ممکن است سربار بیشتری داشته باشد.

در ساخت نخ‌آزاد، mimalloc حافظه را در تعدادی هیپ جداگانه مدیریت می‌کند (در حال حاضر چهار). برای مثال، همه اشیایی که از GC پشتیبانی می‌کنند، از هیپ خودشان تخصیص داده می‌شوند. استفاده از هیپ‌های جداگانه به این معناست که حافظه آزاد در یک هیپ نمی‌تواند برای تخصیصی به‌کار رود که از هیپ دیگری استفاده می‌کند. همچنین، برخی هیپ‌ها به‌گونه‌ای پیکربندی شده‌اند که هنگام آزادسازی حافظه‌ای که زیربنای هیپ است (و در اصطلاح mimalloc به آن «page» گفته می‌شود)، از QSBR (بازپس‌گیری مبتنی بر حالت سکون) استفاده کنند. استفاده از QSBR بین آزاد شدن همه بلوک‌های حافظه یک صفحه و رهاسازی صفحه حافظه، چه برای تخصیص‌های جدید و چه برای بازگشت به سیستم‌عامل، تأخیر ایجاد می‌کند.

تخصیص‌دهنده‌ی mimalloc همچنین بازگرداندن حافظه‌ی آزادشده به سیستم‌عامل را به تعویق می‌اندازد. شما می‌توانید این تأخیر را با تنظیم متغیر محیطی MIMALLOC_PURGE_DELAY روی 0 کاهش دهید. توجه داشته باشید که این کار احتمالاً عملکرد تخصیص‌دهنده را کاهش می‌دهد.

شمارش ارجاع نخ‌آزاد می‌تواند باعث شود اشیاء بیشتر زنده بمانند

در ساخت پیش‌فرض، وقتی شمار ارجاع یک شیء به صفر می‌رسد، آن شیء معمولاً آزاد می‌شود. ساخت نخ‌آزادی از «شمارش ارجاع سوگیرانه» (biased reference counting) استفاده می‌کند که دارای یک مسیر سریع برای اشیاء «متعلق» به نخ جاری و یک مسیر کند برای اشیاء دیگر است. برای جزئیات بیشتر PEP 703 را ببینید. هر زمان که شمار ارجاع یک شیء در وضعیت «در صف» (queued) قرار گیرد، آزادسازی می‌تواند به تعویق بیفتد. وضعیت در صف از بخش «eval breaker» ارزیاب بایت‌کد پاک می‌شود.

ساخت نخ‌آزاد همچنین حالت متفاوتی از شمارش ارجاع را فراهم می‌کند که با نام «شمارش ارجاع معوق (deferred reference counting)» شناخته می‌شود. این حالت با تنظیم پرچمی به‌ازای هر شیء فعال می‌شود. شمارش ارجاع معوق برای انواع زیر فعال است:

  • اشیای ماژول

  • توابع سطح بالای ماژول

  • متدهای کلاس تعریف‌شده در محدوده کلاس

  • اشیای توصیف‌گر

  • اشیای محلی نخ، ایجادشده توسط threading.local

هنگامی که شمارش ارجاع معوق (deferred reference counting) فعال باشد، ارجاع‌ها از پشته‌های تابع پایتون به شمار ارجاع‌ها افزوده نمی‌شوند. این سازوکار سربار شمارش ارجاع را کاهش می‌دهد، به‌ویژه برای اشیایی که توسط چندین نخ استفاده می‌شوند. از آن‌جا که ارجاع‌های پشته شمرده نمی‌شوند، اشیایی که شمارش ارجاع تعویقی دارند، هنگامی که شمار ارجاع داخلی آن‌ها به صفر می‌رسد، بلافاصله آزاد نمی‌شوند. در عوض، آن‌ها در اجرای بعدی زباله‌روبی بررسی می‌شوند و اگر هیچ ارجاعی از پشته به آن‌ها یافت نشود، آزاد می‌شوند. این یعنی این شیء‌ها به‌جای آزاد شدن مانند حالت معمول با صفر شدن شمار ارجاعشان، توسط زباله‌روبی آزاد می‌شوند.

شمارش ارجاع به‌ازای هر نخ می‌تواند آزادسازی اشیاء را به تأخیر بیندازد

برای اجتناب از رقابت بر سر فیلدهای شمارش ارجاع اشیایی که به‌طور مکرر به اشتراک گذاشته می‌شوند، ساخت نخ‌آزاد نیز از «شمارش ارجاع به‌ازای هر نخ» برای چند نوع شیء انتخاب‌شده استفاده می‌کند. به‌جای به‌روزرسانی یک شمارش ارجاع مشترک واحد، هر نخ آرایه شمارش ارجاع محلی خود را نگه‌داری می‌کند؛ این آرایه با شناسه‌ای یکتا که به شیء اختصاص داده شده است، اندیس‌دهی می‌شود. شمارش ارجاع واقعی تنها زمانی با جمع‌کردن شمارش‌های به‌ازای هر نخ محاسبه می‌شود که شمارش محلی شیء به ۰ برسد. شمارش ارجاع به‌ازای هر نخ در حال حاضر برای موارد زیر استفاده می‌شود:

  • اشیای نوع هیپ (کلاس‌های ایجادشده در پایتون)

  • اشیای کد

  • __dict__ اشیای ماژول

از آن‌جا که شمارش‌های ارجاع به‌ازای هر نخ باید پیش از امکان آزادسازی شیء، دوباره با آن ادغام شوند، اشیایی که از شمارش ارجاع به‌ازای هر نخ استفاده می‌کنند معمولاً دیرتر از زمانی آزاد می‌شوند که در ساخت پیش‌فرض آزاد می‌شدند. به‌ویژه، چنین شیئی معمولاً تا زمانی آزاد نمی‌شود که نخی که به آن ارجاع داده است به یک نقطه امن برسد (برای مثال، در بخش «eval breaker» ارزیاب بایت‌کد) یا خارج شود. اجرای gc.collect() شمارش‌های ارجاع به‌ازای هر نخ را ادغام می‌کند و اجازه می‌دهد این اشیاء آزاد شوند.