GIL یا Global Interpreter Lock در پایتون چیست؟

گردآوری و تالیف : ارسطو عباسی
تاریخ انتشار : 29 خرداد 1398
دسته بندی ها : پایتون

به صورتی بسیار ساده GIL یا Global Interpret Lock کاری می‌کند که تنها یک رشته بتواند کنترل مفسر پایتون را در اختیار بگیرد.

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

از آنجایی که GIL تنها اجازه به استفاده از یک رشته را حتی در معماری‌های چند-رشته‌ای می‌دهد، در دنیای پایتون بسیار بدنام شده و به عنوان یکی از ویژگی‌های منفی پایتون از آن یاد می‌شود.

در این مطلب قصد داریم بیشتر با تاثیراتی که GIL می‌تواند روی کارایی برنامه داشته باشد، آشنا شویم و در نهایت نیز روش‌هایی را بررسی می‌کنیم که باعث می‌شوند تا تاثیر GIL روی کدهای‌تان تا حدی کاهش یابد.

GIL چه مشکلی را در پایتون حل می‌کند؟

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

بیایید با یک مثال این موضوع را بهتر نشان دهیم:

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

در کدهای بالا، تعداد Reference Count مربوط به a که یک لیست خالی است ۳ شده. متغیر a یک بار ایجاد شده، یک بار با b مقداردهی شده و یک بار دیگر نیز توسط تابع دریافت میزان ارجاعات فراخوانی شده است.

مشکلی که در چنین سیستم Reference Counting وجود دارد موضوع Race Condition است. این اتفاق زمانی می‌افتد که دو رشته به صورت همزمان قصد داشته باشند که یک مقدار برای مثال مقدار a را کاهش یا افزایش دهند. این اتفاق باعث می‌شود که ناهمخوانی وجود داشته باشد و حافظه هیچوقت نتواند مقدار درستی را به خروجی بفرستد. این اتفاق می‌تواند در نهایت باعث کرش برنامه و یا باگ‌های عجیب و غریب شود.

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

اما GIL یک قفل موجود روی خود مفسر است که قواعدی را برای اجرای تمام کدهای پایتون اعمال می‌کند. این موضوع باعث می‌شود که دیگر نگران Deadlocks نباشیم. اما استفاده از GIL عملا هر برنامه چند-رشته‌ای را به صورت یک برنامه تک-رشته‌ای اجرا می‌کند. 

البته GIL تنها مختص به پایتون نیست، زبان Ruby نیز تا حد زیادی از همین ساختار استفاده می‌کند. زبان‌های دیگر برای حل مشکل Reference Counting از تکنیک‌های دیگری مانند Garbage Collection استفاده می‌کنند. 

چرا GIL به عنوان راه‌حل ارائه شد؟

شاید فکر کنید که انتخاب GIL کار اشتباهی بوده اما به گفته Larry Hastings طراحی و استفاده از GIL یکی از دلایل بسیار مهم موفق بودن امروز پایتون است. 

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

در آن زمان کتابخانه‌های بسیار زیادی ساخته شده‌اند که عملا از زبان C استفاده می‌کردند و برای کار با آن‌ها به یک مدیریت حافظه thread-safe نیاز بود که GIL آن را فراهم می‌کرد. 

پیاده‌سازی GIL کار ساده بود و اضافه کردن آن به پایتون نیز با مشکلات زیادی همراه نبود. همچنین به شدت کارایی برنامه‌هایی که از یک رشته استفاده می‌کردند در پایتون بالا رفت. 

کتابخانه‌های C که thread-safe نبودند به آرامی روال ادغام‌سازی راحت‌تری با پایتون پیدا کردند. این کتابخانه‌ها یکی از دلایل اصلی ورود پایتون به جامعه‌های مختلف نرم‌افزاری بود.

همانطور که مشاهده می‌کنید GIL یک راه‌حل عملگرایانه برای مسئله دشواری بود که توسعه‌دهندگان Cpython با آن درگیر شده بودند.

تاثیر روی برنامه‌های چند-نخی

زمانی که به یک برنامه معمولی کامپیوتری و منحصرا پایتونی نگاه می‌اندازید، آن‌ها در دو دسته‌بندی قرار می‌گیرند، یا CPU-bound هستند و یا I/O-bound. 

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

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

بیایید به یک برنامه ساده از دسته اول نگاه کنیم. این برنامه به روش‌های تک-نخی و چند-نخی نوشته شده است. در آخر قصد داریم میزان زمان‌های اجرای آن را با همدیگر مقایسه کنیم.

# single_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

start = time.time()
countdown(COUNT)
end = time.time()

print('Time taken in seconds -', end - start)

با اجرای این برنامه خروجی زیر را می‌توانید مشاهده بکنید:

$ python single_threaded.py
Time taken in seconds - 6.20024037361145

حال کد را کمی تغییر دهیم و به شکل یک برنامه با دو نخ در بیاوریم:

# multi_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

t1 = Thread(target=countdown, args=(COUNT//2,))
t2 = Thread(target=countdown, args=(COUNT//2,))

start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()

print('Time taken in seconds -', end - start)

زمانی که برنامه را دوباره اجرا کنیم:

$ python multi_threaded.py
Time taken in seconds - 6.924342632293701

همانطور که مشاهده می‌کنید، هر دو برنامه تا حدی یک خروجی را ارائه می‌کنند. در نسخه چند-رشته‌ای GIL از اجرای رشته‌های CPU-bound در حالت پارالل جلوگیری می‌کند.

اما در یک برنامه I/O bound فرایند GIL تاثیر زیادی روی برنامه نخواهد داشت چرا که lock روی رشته‌های مختلفی که منتظر I/O هستند به اشتراک گذاشته شده است.

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

چرا GIL را از پایتون حذف نمی‌کنند؟

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

البته قبلا تلاش‌هایی برای این موضوع انجام گرفت، اما به شدت روی کتابخانه‌های نوشته شده با C تاثیر گذاشت و روند اجرای آن‌ها را با مشکل همراه کرد.

مطمئنا تا به حال جایگزین‌هایی برای GIL پیدا شده است اما مشکل از آنجا بود ک هاین جایگزین‌ها به شدت روی فرایندهای I/O bound تاثیر گذاشت و سرعت و اجرای آن‌ها را کم کرد.

گیدو وان روسوم سازنده پایتون در سال ۲۰۰۷ در یک مقاله گفته بود: من از ارائه ایده جدید برای جایگزین GIL حمایت می‌کنم اما به شرط آنکه جایگزین روی فرایندهای I/O تاثیر نگذارد.

چرا GIL از پایتون ۳ حذف نشد؟

پایتون ۳ فرصت بسیار خوبی برای ایجاد تغییرات بزرگ بود. اما واقعا چرا GIL از آن حذف نشد؟

حذف کردن GIL از پایتون ۳ باعث می‌شد که سرعت اجرای برنامه‌ها در آن نسبت به پایتون ۲ کاهش پیدا کند و این واقعا برنامه‌ای نبود که قابل اجرا باشد.

اما پایتون ۳ بهبودهایی را به GIL نیز آورد. ما در بالا در رابطه با شیوه برخورد GIL با برنامه‌هایی که I/O bound و یا CPU-bound هستند صحبت کردیم. اما اگر یک برنامه ترکیبی از این دو مورد باشد چه؟

در چنین برنامه‌هایی GIL معروف بود که فرایندهای I/O را به گرسنگی می‌کشاند چرا که اولویت بالاتری را برای پردازش‌های CPU-bound در نظر می‌گرفت. 

این بدلیل ساختاری بود که در پایتون وجود داشت. در این ساختار GIL در دوره‌های ثابتی تلاش داشت تا رشته‌ی آزاد را به یک فرایند دیگر بدهد و از آنجایی که فرایند دیگری در کار نبود همان فرایند رشته را در اختیار می‌گرفت.

این مشکل از آنجایی بود که این دوره ثابت نمی‌توانست I/O را به عنوان یک فرایند به GIL معرفی بکند و در نهایت تنها فرایندهای CPU-bound اجرا را در اختیار می‌گرفتند.

این مشکل در ۲۰۰۹ در نسخه ۳.۲ توسط Antoine Pitrou حل شد. در مکانیسم جدید رشته‌هایی که درخواست استفاده از GIL را داشتند اما از دوره حذف شدند را بررسی می‌کردند و به آن‌ها شانسی برای اجرا می‌دادند.

چگونه با GIL تعامل داشته باشیم

اگر GIL برای شما دردسرساز است بهتر است این کارها را انجام دهید:

استفاده از multi-processing بجای multi-threading: بجای استفاده از thread از processها استفاده کنید. در این روش هر فرایند پایتونی مفسر منحصر به فرد خود را دارد و مشکلات multi-threading را ندارد.

from multiprocessing import Pool
import time

COUNT = 50000000
def countdown(n):
    while n>0:
        n -= 1

if __name__ == '__main__':
    pool = Pool(processes=2)
    start = time.time()
    r1 = pool.apply_async(countdown, [COUNT//2])
    r2 = pool.apply_async(countdown, [COUNT//2])
    pool.close()
    pool.join()
    end = time.time()
    print('Time taken in seconds -', end - start)

خروجی این برنامه:

$ python multiprocess.py
Time taken in seconds - 4.060242414474487

همانطور که مشاهده می‌کنید، این حالت بسیار سریع تر کارها را نسبت به روال‌های قبلی انجام می‌دهد.

از جایگزین‌های Cpython استفاده کنید: پایتون زبانی است که نسخه‌های پیاده‌سازی بسیار زیادی دارد. جایتون، سایتون و پای‌پای از این دست هستند. هر کدام این پیاده‌سازی‌ها ویژگی‌ منحصر به فرد خود را دارند که می‌توانید از آن‌ها استفاده کنید.

منتظر باشید: تلاش‌های مختلفی برای حذف GIL در حال انجام است. Gilectomy یکی از آن تلاش‌هاست. مطمئنا تیم توسعه پایتون نیز در حال تلاش‌های برای بهبود وضعیت هستند.

نیازی نیست که همگی در ارتباط با GIL نگران باشند. شما تنها زمانی با مشکل کارایی مواجه می‌شوید که از کتابخانه‌های C استفاده کنید و یا آنکه برنامه‌های چند-نخی CPU-bound داشته باشید.

اگر قصد دانستن بیشتر در ارتباط با مسئله GIL دارید پیشنهاد می‌شود که ویدیو David Beazley را مشاهده کنید.

منبع

مقالات پیشنهادی

  • ایجاد Website Blocker با استفاده از پایتون

    درست است که اپلیکیشن‌های مختلف بسیاری برای بلاک کردن وبسایت‌ها وجود دارد، اما چرا وقتی خودتان می‌توانید آن را بنویسید، سراغ دیگر اپلیکیشن‌ها می‌روید؟...

    ارسطو عباسی