توسعه‌دهندگان پایتون سال‌هاست با یک پارادوکس پیچیده دست‌وپنجه نرم می‌کنند: بهره‌وری خیره‌کننده و کتابخانه‌های بی‌شمار از یک سو، و محدودیت‌های شدید در استفاده از پردازنده‌های چند هسته‌ای (Multicore) از سوی دیگر. مقصر اصلی این گلوگاه تاریخی، مکانیزمی به نام قفل مفسر جهانی یا GIL (Global Interpreter Lock) است.

در هسته اصلی پایتون (CPython)، تنها یک ریسمان (Thread) بومی می‌تواند در هر لحظه کدهای بایت‌کد (Bytecode) را اجرا کند. این محدودیت برای کارهای ورودی/خروجی (I/O) مانند دانلود فایل یا درخواست‌های شبکه مشکلی ایجاد نمی‌کند، اما بلندمرتبه‌ترین مانع را برای گردش‌کارهای وابسته به پردازنده (CPU-bound) به شمار می‌رود. GIL در واقع یک قفل انحصاری متقابل (Mutex) است که از اشیای پایتون محافظت می‌کند. می‌توان آن را شبیه یک کلید واحد برای مفسر در نظر گرفت که در هر لحظه تنها در اختیار یک ریسمان قرار دارد.

نمایش گرافیکی قفل مفسر جهانی یا GIL در پایتون

ریشه‌های تاریخی GIL؛ انتخابی هوشمندانه نه یک باگ

قفل مفسر جهانی هرگز برای اذیت کردن برنامه‌نویسان طراحی نشد. این مکانیزم در روزهای ابتدایی شکل‌گیری CPython برای حل یک چالش بنیادین در مدیریت حافظه معرفی شد: شمارش مرجع (Reference Counting). در پایتون، هر شیء اعم از اعداد، رشته‌ها یا لیست‌ها، دقیقا ردیابی می‌شود که چند متغیر به آن ارجاع داده‌اند. به محض اینکه این شمارش به صفر برسد، حافظه آن شیء فورا آزاد می‌شود.

اگر چندین ریسمان بخواهند به طور همزمان شمارش مرجع یک شیء را تغییر دهند، آشفتگی حافظه رخ می‌دهد. این تداخل می‌تواند منجر به نشت حافظه (Memory Leak) یا حتی خطای قطعه‌بندی (Segmentation Fault) و کرش کردن کامل برنامه شود. راهکار زورگویانه اما به شدت مؤثر توسعه‌دهندگان پایتون، ایجاد یک قفل جهانی بود تا تضمین شود در هر لحظه تنها یک ریسمان با اشیای در حال اجرا تعامل دارد.

گیدو ون روسام (Guido van Rossum)، خالق پایتون، در اواخر دهه 1980 یک تصمیم کاملا عمل‌گرایانه گرفت. GIL به زیبایی از بروز کلیه مشکلات شرایط مسابقه (Race Condition) در مدیریت حافظه جلوگیری می‌کرد. افزون بر این، بسیاری از کتابخانه‌های قدرتمند زبان C که پایتون برای رشد اکوسیستم خود به آن‌ها وابسته بود، فاقد ایمنی ریسمان (Thread Safety) بودند. این قفل مانند یک تور امنیتی عمل کرد و ادغام ماژول‌های C را در پایتون به شدت ساده کرد؛ عاملی که پیدایش کتابخانه‌های نگارش علمی نظیر NumPy و SciPy را تسریع بخشید.

3 گلوگاه اصلی قفل مفسر جهانی

در سطح عملیاتی، GIL بیشترین آسیب را در سه حوزه زیر به جا می‌گذارد:

  • محاسبات سنگین پردازنده: پردازش‌های عددی طولانی، پردازش تصویر و کار با گراف‌ها. تلاش برای توزیع این وظایف میان چندین ریسمان باعث رقابت آن‌ها برای کسب قفل شده و سرعت اجرای کد را حتی از حالت تک‌ریسمانی (Single-thread) نیز پایین‌تر می‌آورد.
  • پردازش داده‌ها با نرخ انتقال بالا: تصور کنید یک سرویس وبِ پُربازدید در حال مدیریت هزاران درخواست در ثانیه است. اگرچه انتقال داده‌های شبکه مشکلی ندارد، اما پردازش منطق تجاری و کدگذاری فرمت‌هایی مانند JSON به شدت به CPU وابسته‌اند.
  • تفرقه در اکوسیستم توسعه: از آنجا که ریسمان‌ها به صورت پیش‌فرض مقیاس‌پذیر نبودند، جامعه کاربری پایتون به سمت ابزارهای مبتنی بر فرآیند (Process) سوق پیدا کرد و الگوهای غنی همزمانی (Concurrency) با حافظه مشترک، که در زبان‌هایی نظیر Go یا Java بسیار رایج است، در پایتون کمتر شکوفا شد.
نمودار مقایسه پردازش موازی و گلوگاه GIL در پایتون

راهکار Multiprocessing و هزینه‌های پنهان آن

توسعه‌دهندگان برای دور زدن این محدودیت بی‌آلایش نیستند. ماژول multiprocessing به برنامه‌نویسان اجازه می‌دهد تا به موازات واقعی (True Parallelism) دست یابند. این ماژول به جای ایجاد ریسمان جدید، فرآیندهای (Process) مجزای پایتون را اجرا می‌کند که هر کدام مفسر و GIL اختصاصی خود را دارند. با این حال، این راهکار جایگزین هزینه‌های سنگینی در پی دارد.

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

با معرفی پیشنهادهایی نظیر PEP 703 برای حذف اختیاری GIL در نسخه‌های آینده، پایتون در آستانه یک تحول بزرگ در معماری همزمانی خود قرار دارد. موفقیت این تغییرات تا حد زیادی به نحوه بازنویسی و سازگاری کتابخانه‌های فعلی وابسته است که آینده پردازش موازی در این زبان را شکل خواهند داد.