چندین مفسر در یک فرایند پایتون

هرچند در بیشتر موارد شما تنها یک مفسر پایتون را تعبیه می‌کنید، مواردی وجود دارد که نیاز دارید چندین مفسر مستقل را در همان فرایند و شاید حتی در همان نخ ایجاد کنید. زیرمفسرها به شما امکان انجام این کار را می‌دهند.

مفسر «اصلی» نخستین مفسری است که هنگام راه‌اندازی ران‌تایم ایجاد می‌شود. این مفسر معمولاً تنها مفسر پایتون در یک فرایند است. برخلاف زیرمفسرها، مفسر اصلی مسئولیت‌های منحصربه‌فردی در سطح کل فرایند مانند مدیریت سیگنال را بر عهده دارد. این مفسر همچنین مسئول اجرا در طول راه‌اندازی ران‌تایم است و معمولاً مفسر فعال در طول نهایی‌سازی ران‌تایم است. تابع PyInterpreterState_Main() اشاره‌گری به وضعیت آن برمی‌گرداند.

می‌توانید با استفاده از تابع PyThreadState_Swap() بین زیرمفسرها جابه‌جا شوید. می‌توانید آن‌ها را با استفاده از توابع زیر ایجاد و نابود کنید:

type PyInterpreterConfig

ساختاری که بیشتر پارامترهای لازم برای پیکربندی یک زیرمفسر را در بر دارد. مقادیر آن تنها در Py_NewInterpreterFromConfig() استفاده می‌شوند و هرگز توسط ران‌تایم تغییر داده نمی‌شوند.

اضافه شده در نسخه‌ی 3.12.

فیلد‌های ساختار:

int use_main_obmalloc

اگر این مقدار 0 باشد، زیرمفسر از وضعیت تخصیص‌دهنده‌ی «object» خود استفاده می‌کند. در غیر این صورت از وضعیت تخصیص‌دهنده‌ی مفسر اصلی (به اشتراک) استفاده می‌کند.

اگر این مقدار 0 باشد، آنگاه check_multi_interp_extensions باید 1 (ناصفر) باشد. اگر این مقدار 1 باشد، آنگاه gil نباید PyInterpreterConfig_OWN_GIL باشد.

int allow_fork

اگر این مقدار 0 باشد، ران‌تایم از فورک کردن فرایند در هیچ نخی که زیرمفسر در حال حاضر در آن فعال است پشتیبانی نخواهد کرد. در غیر این صورت، فورک بدون محدودیت است.

توجه داشته باشید که ماژول subprocess وقتی فورک ممنوع است، همچنان کار می‌کند.

int allow_exec

اگر این مقدار 0 باشد، ران‌تایم در هیچ نخی که زیرمفسر در حال حاضر در آن فعال است، از جایگزینی فرایند جاری از طریق exec (مثلاً os.execv()) پشتیبانی نخواهد کرد. در غیر این صورت، exec بدون محدودیت است.

توجه داشته باشید که ماژول subprocess هنگامی که exec ممنوع است، همچنان کار می‌کند.

int allow_threads

اگر این مقدار 0 باشد، ماژول threading زیرمفسر نخ‌ها را ایجاد نمی‌کند. در غیر این صورت، نخ‌ها مجاز هستند.

int allow_daemon_threads

اگر این مقدار 0 باشد، ماژول threading در زیرمفسر نخ‌های دِیمِن ایجاد نمی‌کند. در غیر این صورت، نخ‌های دِیمِن مجاز هستند (تا زمانی که allow_threads ناصفر باشد).

int check_multi_interp_extensions

اگر این مقدار 0 باشد، همه ماژول‌های توسعه‌ای، از جمله ماژول‌های قدیمی (با مقداردهی اولیه تک‌مرحله‌ای)، می‌توانند در هر نخی که زیرمفسر در حال حاضر در آن فعال است ایمپورت شوند. در غیر این صورت، فقط ماژول‌های توسعه‌ای با مقداردهی اولیه چندمرحله‌ای (نگاه کنید به PEP 489) می‌توانند ایمپورت شوند. (همچنین Py_mod_multiple_interpreters را ببینید.)

اگر use_main_obmalloc برابر 0 باشد، این مقدار باید 1 (نا‌صفر) باشد.

int gil

این، عملکرد قفل مفسر سراسری را برای زیرمفسر تعیین می‌کند. این می‌تواند یکی از موارد زیر باشد:

PyInterpreterConfig_DEFAULT_GIL

از انتخاب پیش‌فرض (PyInterpreterConfig_SHARED_GIL) استفاده کنید.

PyInterpreterConfig_SHARED_GIL

استفاده از (اشتراک‌گذاری) قفل مفسر سراسریِ مفسر اصلی.

PyInterpreterConfig_OWN_GIL

از قفل مفسر سراسریِ خودِ زیرمفسر استفاده کنید.

اگر این مقدار PyInterpreterConfig_OWN_GIL باشد، آنگاه PyInterpreterConfig.use_main_obmalloc باید 0 باشد.

PyStatus Py_NewInterpreterFromConfig(PyThreadState **tstate_p, const PyInterpreterConfig *config)

یک زیرمفسر جدید ایجاد کنید. این یک محیط (تقریباً) کاملاً جداگانه برای اجرای کد پایتون است. به‌ویژه، مفسر جدید نسخه‌های جداگانه و مستقلی از تمام ماژول‌های ایمپورت‌شده دارد، از جمله ماژول‌های بنیادی builtins، __main__ و sys. جدول ماژول‌های بارگذاری‌شده (sys.modules) و مسیر جستجوی ماژول (sys.path) نیز جداگانه هستند. محیط جدید متغیر sys.argv ندارد. این محیط اشیاء پرونده‌ی جدید جریان‌های ورودی/خروجی استاندارد sys.stdin، sys.stdout و sys.stderr را دارد (البته این‌ها به همان توصیف‌گرهای پرونده‌ی زیرین ارجاع می‌دهند).

config داده‌شده، گزینه‌هایی را که مفسر با آن‌ها راه‌اندازی می‌شود کنترل می‌کند.

در صورت موفقیت، tstate_p به نخستین وضعیت نخ ایجادشده در زیرمفسر جدید تنظیم می‌شود. این وضعیت نخ متصل است. توجه داشته باشید که هیچ نخ واقعی ایجاد نمی‌شود؛ بحث درباره وضعیت‌های نخ را در ادامه ببینید. اگر ایجاد مفسر جدید ناموفق باشد، tstate_p به NULL تنظیم می‌شود؛ هیچ استثنایی تنظیم نمی‌شود، زیرا وضعیت استثنا در وضعیت نخ متصل ذخیره می‌شود که ممکن است وجود نداشته باشد.

مانند تمام توابع دیگر Python/C API، باید پیش از فراخوانی این تابع، یک attached thread state موجود باشد، اما ممکن است هنگام بازگشت جدا شده باشد. در صورت موفقیت، وضعیت نخ بازگردانده‌شده متصل خواهد بود. اگر زیرمفسر (sub-interpreter) با GIL اختصاصی خودش ایجاد شود، attached thread state مفسر فراخواننده جدا خواهد شد. هنگامی که تابع بازمی‌گردد، thread state مفسر جدید به نخ فعلی متصل خواهد بود و attached thread state مفسر قبلی جدا باقی خواهد ماند.

اضافه شده در نسخه‌ی 3.12.

زیرمفسرها زمانی بیشترین کارایی را دارند که از یکدیگر ایزوله باشند و برخی از قابلیت‌ها محدود باشند:

PyInterpreterConfig config = {
    .use_main_obmalloc = 0,
    .allow_fork = 0,
    .allow_exec = 0,
    .allow_threads = 1,
    .allow_daemon_threads = 0,
    .check_multi_interp_extensions = 1,
    .gil = PyInterpreterConfig_OWN_GIL,
};
PyThreadState *tstate = NULL;
PyStatus status = Py_NewInterpreterFromConfig(&tstate, &config);
if (PyStatus_Exception(status)) {
    Py_ExitStatusException(status);
}

توجه داشته باشید که پیکربندی فقط به‌طور گذرا استفاده می‌شود و تغییر داده نمی‌شود. در طول مقداردهی اولیه، مقادیر پیکربندی به مقادیر مختلف PyInterpreterState تبدیل می‌شوند. ممکن است یک نسخه‌ی فقط‌خواندنی از پیکربندی به‌طور داخلی روی PyInterpreterState ذخیره شود.

ماژول‌های توسعه‌ای بین (زیر-)مفسر‌ها به شرح زیر به اشتراک گذاشته می‌شوند:

  • برای ماژول‌هایی که از مقداردهی اولیه چندمرحله‌ای استفاده می‌کنند، مانند PyModule_FromDefAndSpec()، یک شیء ماژول جداگانه برای هر مفسر ایجاد و مقداردهی اولیه می‌شود. تنها متغیرهای ایستا و سراسری در سطح C بین این اشیاء ماژول به اشتراک گذاشته می‌شوند.

  • برای ماژول‌هایی که از مقداردهی اولیه تک‌فازی قدیمی استفاده می‌کنند، مانند PyModule_Create()، اولین باری که یک ماژول توسعه‌ای خاص ایمپورت می‌شود، به‌طور عادی مقداردهی اولیه می‌شود و یک کپی (سطحی) از دیکشنری ماژول آن ذخیره می‌شود. وقتی همان ماژول توسعه‌ای توسط (زیر)مفسر دیگری ایمپورت می‌شود، یک ماژول جدید مقداردهی اولیه می‌شود و با محتویات این کپی پر می‌شود؛ تابع init ماژول توسعه‌ای فراخوانی نمی‌شود. بنابراین، اشیاء موجود در دیکشنری ماژول در نهایت میان (زیر)مفسر‌ها به اشتراک گذاشته می‌شوند، که ممکن است باعث رفتار ناخواسته شود (به باگ‌ها و هشدار‌ها در ادامه مراجعه کنید).

    توجه داشته باشید که این مورد با آنچه هنگام ایمپورت شدن یک ماژول توسعه‌ای پس از بازمقداردهی اولیه‌ی کامل مفسر از طریق فراخوانی Py_FinalizeEx() و Py_Initialize() رخ می‌دهد، متفاوت است؛ در آن صورت، تابع initmodule ماژول توسعه‌ای دوباره فراخوانی می‌شود. مانند مقداردهی اولیه‌ی چندمرحله‌ای، این بدان معناست که تنها متغیرهای ایستا و سراسری در سطح C بین این ماژول‌ها مشترک هستند.

PyThreadState *Py_NewInterpreter(void)
قسمتی از ABI پایدار.

یک زیرمفسر (sub-interpreter) جدید ایجاد کنید. این در اصل صرفاً پوششی در اطراف Py_NewInterpreterFromConfig() با پیکربندی‌ای است که رفتار موجود را حفظ می‌کند. نتیجه، یک زیرمفسر غیرمجزا (unisolated) است که قفل مفسر سراسری (GIL) را با مفسر اصلی به اشتراک می‌گذارد، اجازه‌ی fork/exec را می‌دهد، اجازه‌ی نخ‌های دِیمِن را می‌دهد و اجازه‌ی ماژول‌های مقداردهی اولیه‌ی تک‌فازی (single-phase init) را می‌دهد.

void Py_EndInterpreter(PyThreadState *tstate)
قسمتی از ABI پایدار.

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

Py_FinalizeEx() تمام زیرمفسرهایی را که تا آن لحظه به‌طور صریح نابود نشده‌اند، از بین می‌برد.

قفل مفسر سراسری (GIL) به‌ازای هر مفسر

اضافه شده در نسخه‌ی 3.12.

با استفاده از Py_NewInterpreterFromConfig() می‌توانید یک زیرمفسر ایجاد کنید که به‌طور کامل از سایر مفسرها جداشده باشد، از جمله با داشتن قفل مفسر سراسری مخصوص به خود. مهم‌ترین مزیت این جداسازی این است که چنین مفسری می‌تواند بدون مسدود شدن توسط مفسرهای دیگر و بدون مسدود کردن هیچ مفسر دیگری، کد پایتون را اجرا کند. بدین ترتیب، یک فرایند پایتون واحد می‌تواند هنگام اجرای کد پایتون واقعاً از چندین هسته‌ی CPU بهره ببرد. این جداسازی همچنین به‌جای صرفاً استفاده از نخ‌ها، رویکردی متفاوت به همزمانی را ترغیب می‌کند. (به PEP 554 و PEP 684 مراجعه کنید.)

استفاده از مفسر مجزا نیازمند هوشیاری در حفظ آن جداسازی است. این به‌ویژه به آن معناست که هیچ شیئی یا وضعیت تغییرپذیری را نباید بدون تضمین‌هایی درباره‌ی نخ‌ایمنی به اشتراک گذاشت. حتی اشیائی که در سایر جهات تغییرناپذیرند (مانند None و (1, 5)) به‌طور معمول نمی‌توانند به دلیل شمارش ارجاع به اشتراک گذاشته شوند. یک راه‌حل ساده اما کم‌کارآمدتر برای این مسئله، استفاده از یک قفل سراسری در پیرامون تمامی استفاده‌ها از یک وضعیت (یا شیء) است. به‌طور جایگزین، اشیاء عملاً تغییرناپذیر (مانند اعداد صحیح یا رشته‌ها) را می‌توان با وجود شمارش ارجاعشان، از طریق نامیرا کردن آنها ایمن ساخت. در واقع، این کار برای تک‌نمونه‌های توکار، اعداد صحیح کوچک و تعدادی از اشیاء توکار دیگر انجام شده است.

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

علاوه بر این، یکی از چالش‌های اصلی استفاده از چندین مفسر جداشده این است که چگونه می‌توان به‌طور امن (بدون بر هم زدن جداسازی) و کارآمد بین آن‌ها ارتباط برقرار کرد. ران‌تایم و کتابخانه استاندارد هنوز هیچ رویکرد استانداردی برای این کار ارائه نمی‌دهند. یک ماژول آینده در کتابخانه استاندارد می‌تواند به کاهش تلاش لازم برای حفظ جداسازی کمک کند و ابزارهای مؤثری برای ارتباط (و اشتراک‌گذاری) داده‌ها بین مفسرها در اختیار بگذارد.

اشکال‌ها و هشدارها

از آنجا که زیرمفسر‌ها (و مفسر اصلی) بخشی از یک فرایند واحد هستند، جداسازی میان آن‌ها بی‌نقص نیست --- برای مثال، با استفاده از عملیات‌های سطح پایین پرونده مانند os.close() می‌توانند (به‌طور تصادفی یا خرابکارانه) بر پرونده‌های باز یکدیگر تأثیر بگذارند. به دلیل نحوه‌ی اشتراک‌گذاری ماژول‌های توسعه‌ای میان (زیر)مفسر‌ها، ممکن است برخی از ماژول‌های توسعه‌ای به‌درستی کار نکنند؛ احتمال این امر به‌ویژه هنگام استفاده از مقداردهی اولیه‌ی تک‌فازی یا متغیرهای سراسری (ایستا) بیشتر است. می‌توان اشیایی را که در یک زیرمفسر ایجاد شده‌اند، در فضای نام (زیر)مفسر دیگری قرار داد؛ بهتر است در صورت امکان از این کار پرهیز شود.

باید با دقت ویژه از اشتراک‌گذاری توابع، متدها، نمونه‌ها یا کلاس‌های تعریف‌شده توسط کاربر بین زیرمفسر‌ها جلوگیری شود، زیرا عملیات ایمپورت که توسط چنین اشیایی اجرا می‌شوند، ممکن است بر دیکشنری ماژول‌های بارگذاری‌شده‌ی (زیر)مفسر اشتباه تأثیر بگذارد. به همان اندازه مهم است که از اشتراک‌گذاری اشیایی که موارد فوق از طریق آن‌ها قابل دسترسی‌اند، جلوگیری شود.

همچنین توجه داشته باشید که ترکیب این قابلیت با API‌های PyGILState_* امری حساس است، زیرا این API‌ها تناظر یک‌به‌یک بین وضعیت‌های نخ پایتون و نخ‌های سطح سیستم‌عامل را فرض می‌کنند؛ فرضی که با وجود زیرمفسر‌ها نقض می‌شود. قویاً توصیه می‌شود که زیرمفسر‌ها را بین یک جفت فراخوانی متناظر PyGILState_Ensure() و PyGILState_Release() عوض نکنید. علاوه بر این، ماژول‌های توسعه‌ای (مانند ctypes) که از این API‌ها برای فراهم کردن امکان فراخوانی کد پایتون از نخ‌هایی که توسط پایتون ایجاد نشده‌اند استفاده می‌کنند، احتمالاً هنگام استفاده از زیرمفسر‌ها از کار می‌افتند.

APIهای سطح‌بالا

type PyInterpreterState
قسمتی از API محدود (به‌عنوان یک ساختار مبهم).

این ساختار داده وضعیت مشترک بین تعدادی از نخ‌های همکاری‌کننده را نشان می‌دهد. نخ‌هایی که به یک مفسر واحد تعلق دارند، مدیریت ماژول خود و چند مورد داخلی دیگر را به اشتراک می‌گذارند. در این ساختار هیچ عضو عمومی وجود ندارد.

نخ‌های متعلق به مفسرهای مختلف در ابتدا هیچ چیزی را به اشتراک نمی‌گذارند، به‌جز وضعیت فرایند مانند حافظه‌ی در دسترس، توصیف‌گرهای پرونده‌ی باز و مواردی از این دست. قفل مفسر سراسری نیز میان همه‌ی نخ‌ها مشترک است، صرف‌نظر از اینکه به کدام مفسر تعلق دارند.

تغییر یافته در نسخه‌ی 3.12: PEP 684 امکان قفل مفسر سراسری برای هر مفسر را معرفی کرد. Py_NewInterpreterFromConfig() را ببینید.

PyInterpreterState *PyInterpreterState_Get(void)
قسمتی از ABI پایدار از نسخه‌ی 3.9.

مفسر جاری را برمی‌گرداند.

اگر attached thread state وجود نداشته باشد، خطای مهلک صادر می‌کند. این تابع نمی‌تواند NULL را بازگرداند.

اضافه شده در نسخه‌ی 3.9.

int64_t PyInterpreterState_GetID(PyInterpreterState *interp)
قسمتی از ABI پایدار از نسخه‌ی 3.7.

شناسه یکتای مفسر را برمی‌گرداند. اگر در انجام این کار خطایی رخ دهد، -1 برگردانده می‌شود و خطا تنظیم می‌شود.

فراخوان‌کننده باید یک attached thread state داشته باشد.

اضافه شده در نسخه‌ی 3.7.

PyObject *PyInterpreterState_GetDict(PyInterpreterState *interp)
مقدار بازگشتی: مرجع امانتی. قسمتی از ABI پایدار از نسخه‌ی 3.8.

یک دیکشنری برمی‌گرداند که در آن می‌توان داده‌های خاص مفسر را ذخیره کرد. اگر این تابع NULL را برگرداند، هیچ استثنایی ایجاد نشده است و فراخواننده باید فرض کند که هیچ دیکشنری خاص مفسری در دسترس نیست.

این جایگزینی برای PyModule_GetState() نیست؛ ماژول‌های توسعه‌ای باید از آن برای ذخیره‌سازی اطلاعات وضعیت مخصوص مفسر استفاده کنند.

دیکشنری برگردانده‌شده از مفسر امانت گرفته شده است و تا زمان خاموش شدن مفسر معتبر است.

اضافه شده در نسخه‌ی 3.8.

typedef PyObject *(*_PyFrameEvalFunction)(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwflag)

نوع یک تابع ارزیابی فریم.

پارامتر throwflag توسط متد throw() تولیدگرها استفاده می‌شود: اگر ناصفر باشد، استثنای جاری مدیریت می‌شود.

تغییر یافته در نسخه‌ی 3.9: این تابع اکنون پارامتر tstate را می‌پذیرد.

تغییر یافته در نسخه‌ی 3.11: پارامتر frame از PyFrameObject* به _PyInterpreterFrame* تغییر کرد.

_PyFrameEvalFunction _PyInterpreterState_GetEvalFrameFunc(PyInterpreterState *interp)

تابع ارزیابی فریم را دریافت کنید.

به PEP 523 «افزودن یک API ارزیابی فریم به سی‌پایتون» مراجعه کنید.

اضافه شده در نسخه‌ی 3.9.

void _PyInterpreterState_SetEvalFrameFunc(PyInterpreterState *interp, _PyFrameEvalFunction eval_frame)

تابع ارزیابی فریم را تنظیم می‌کند.

به PEP 523 «افزودن یک API ارزیابی فریم به سی‌پایتون» مراجعه کنید.

اضافه شده در نسخه‌ی 3.9.

API‌های سطح پایین

تمام توابع زیر باید پس از Py_Initialize() فراخوانی شوند.

تغییر یافته در نسخه‌ی 3.7: Py_Initialize() اکنون قفل مفسر سراسری را مقدار‌دهی اولیه می‌کند و یک وضعیت نخ متصل تنظیم می‌کند.

PyInterpreterState *PyInterpreterState_New()
قسمتی از ABI پایدار.

یک شیء وضعیت مفسر جدید ایجاد می‌کند. نیازی به attached thread state نیست، اما در صورت ضرورت سریال‌سازی فراخوانی‌های این تابع، می‌تواند به‌صورت اختیاری وجود داشته باشد.

یک رویداد حسابرسی cpython.PyInterpreterState_New را بدون هیچ آرگومانی ایجاد می‌کند.

void PyInterpreterState_Clear(PyInterpreterState *interp)
قسمتی از ABI پایدار.

تمام اطلاعات در یک شیء وضعیت مفسر را بازنشانی می‌کند. باید یک attached thread state برای مفسر وجود داشته باشد.

رویداد حسابرسی cpython.PyInterpreterState_Clear را بدون هیچ آرگومانی ایجاد می‌کند.

void PyInterpreterState_Delete(PyInterpreterState *interp)
قسمتی از ABI پایدار.

یک شیء وضعیت مفسر را نابود می‌کند. برای مفسر هدف نباید attached thread state وجود داشته باشد. وضعیت مفسر باید پیش‌تر با فراخوانی PyInterpreterState_Clear() بازنشانی شده باشد.

پشتیبانی از اشکال‌زدای پیشرفته

این توابع تنها برای استفاده توسط ابزارهای پیشرفته‌ی اشکال‌زدایی در نظر گرفته شده‌اند.

PyInterpreterState *PyInterpreterState_Head()

شیء وضعیت مفسر در ابتدای فهرست همه‌ی چنین اشیایی را بازمی‌گرداند.

PyInterpreterState *PyInterpreterState_Main()

شیء وضعیت مفسر اصلی را برمی‌گرداند.

PyInterpreterState *PyInterpreterState_Next(PyInterpreterState *interp)

شیء وضعیت مفسرِ بعدی پس از interp را از فهرست همه‌ی چنین اشیایی برمی‌گرداند.

PyThreadState *PyInterpreterState_ThreadHead(PyInterpreterState *interp)

اشاره‌گر به نخستین شیء PyThreadState در فهرست نخ‌های مرتبط با مفسر interp را برمی‌گرداند.

PyThreadState *PyThreadState_Next(PyThreadState *tstate)

شیء وضعیت نخ بعدی را پس از tstate از فهرست همه‌ی اشیاء از این دست که به همان شیء PyInterpreterState تعلق دارند، برمی‌گرداند.