Python GIL (Global Interpreter Lock)
Pythonning Global Interpretator blocki bu, sodda qilib aytganda mutex (yoki qulf, bloklash), qaysiki Python interpretatorini faqat bitta potok boshqarishini taminlaydi.
Dasturchi uchun bitta potok ishlayotgani sezilmaydi, lekin ayrim vaqtlardi u CPU da probka hosil qilib unumdorlikni kamaytiradi, yani sekinlashadi, yoki ko'p potokli kodlarda muammolar yuzaga kelishi mumkin.
Python GIL bir vaqtda bittadan ortiq potok ishlashiga ruxsat bermaganligi sababli bittadan ortiq yardoga ega bo'lgan Ko'p Potokli CPU lar Python code uchun ahamiyatsizdek bo'ladi, shuning uchun GIL Pythonning "infamos" xususiyati degan nomni ham olib ulgurgan.
Bu maqolada biz Python GIL ni unumdorlikga salbiy tasiri va bu tasirni qanaqa qilib chetklab o'tsa bo'lishini o'rganamiz.
Pythonga GIL nima uchun kerak ?, u qanaqa muammoni hal qiladi ?
Python xotira boshqaruvi uchun reference count ishlatadi. Yani Pythonda yaratilgan har bir obyektning reference count degan o'zgaruvchisi bo'ladi, shu o'zgaruvchi orqali Python obyekt necha marta ishlatilgani yoki umuman ishlatilmayotgani haqida xabar berib turadi, qachon ushbu count nolga yetsa, shu obyekt band qilib turgan xotira bo'shatiladi.
Qisqacha misol:
>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3
Bu yerda etibor bergan bo'lsangiz bo'sh a listning reference counti 3 ta, birinchisi o'zi yaratilgan holatda, ikkinchisi b o'zgaruvchiga havola berilganda, uchinchisi getrefcount() funksiyasiga argument qilib berilganda.
GIL ga qaytamiz:
Muammo shundaki reference count uchun uni ikkita yoki undan ortiq potok bir vaqtning o'zida ketma ket o'zgatirishini oldini olish uchun himoyachi kerak edi. Agar shu hoalt yuz beradigon bo'lsa, biror obyekt band qilib turgan xotira hech qachon ozod qilinmasligi mumkin, yoki undan ham yomoni, xotirada hali reference counti 0 ga teng bo'lmagan obyektni xotiradan o'chirib yuborishi, bunday xolatlar sizning Python dasturingizda kutilmagan noodatiy baglar paydo bo'ladi.
Reference countni himoya qilish uchun har bir potok bo'ylab tarqatilgan data stukturaga begona potok o'zgartirib yubormasligi uchun block qo'yiladi.
Lekin har bir obyektga yoki obyektlar guruhiga block qo'yish degani bu ko'plab blocklar bir vaqtning o'zida mavjud bo'ladi degani, bu DEADLOCK ga olib keladi (Deadlock bu bittadan ortiq locklar mavjud bo'lganda yuzaga keladi). Boshqa minus esa blocklarning ketma ket qo'yilishi va olinishi sababli yuzaga keladigan unumdorlikdagi muammolardir.
GIL bu interpretatorning o'zidagi block bo'lib, qaysiki har bir Python kode interpretatsiya bo'lishi uchun interpretator lockdan ruxsat so'rash qoidasini o'rnatadi. Bu narsa deadlocklarning oldini oladi (Faqat block bir dona bo'lgan vaqtda) va unchalik unumdorlikga salbiy tasir qilmaydi. Ammo istalgan CPU talab qiladigan Python dasturni bir potokli qilib qo'yadi.
GIL Ruby va shunga o'xshagab bir nechta tillarda ham foydalanilgan bo'lsada u yagona yechim emas. Ayrim dasturlash potokdan himoyalangan xotira boshqaruvi uchun reference countdan boshqa usullardan foydalangan xolda GIL dan foydalanishning oldini olishadi, misol uchun garbage collection.
Bunday dasturlash tillari GIL ning bir potoklilik bilan keltiradigan unumdorligini JIT compaylerlar kabi xususiyatlar orqali kompensatsiya qiladi.
Nima uchun aynan GIL yechim sifatida olingan ?
Python hali kompyuterlarda ko'p potoklilik tushunchasi yo'q bo'lgan davrlardan buyon mavjud. Python developmetni tezlashtirish maqsadida foydalanish uchun oson qilib yaratilgan va shu sababli Pythonni kun sain ko'plab dasturchilar ishlata boshlashgan.
Python uchun kerak bo'lgan C kutubxonalari uchun juda ko'plab extentionlar yozilgan. Nojo'ya o'zgarishlarning oldini olish maqsadida, ushbu C extentionlari uchun GIL taminlab beradigan potokdan himoyalangan xotira boshqaruvi talab etilgan.
GIL ni implement qilish juda oson hamda osongina Pythonga qo'shilgan. GIL bir potokli dasturlarda bizga yuqori unumdorlik beradi, sababi dastur faqat bitta lock ustida ishlaydi.
Potokdan himoyalanmagan C extentionlari oson integratsiya qilingan. Va aynan shu extentionlar Pythonning ko'plab jamoalar tomonidan osongina qabul qilinishining asosidir.
Ko'rib turganingizdek GIL Pythonning boshlanish davrida ishlab chiqaruvchilar yuz tutgan katta muammoning pragmatik yechimi bo'lgan.
Ko'p potokli Python dasturlariga tasiri
Kompyuter dasturlariga qaraganimizda CPU bound dastrlar va I/O bound dasturlarning unumdorligida biroz farq bo'ladi.
CPU bound dasturlar CPU ni maksimum ishlatadi. Misol uchun matematik hisob kitoblar, matretsalarni ko'paytirish, qidiruv, rasmlar ustida operatsiyalar va h.kz.
I/O bound dasturlar esa ko'proq vaqtni Inputni qabul qilish va Outputni yetkazib berishga sarflaydilar. Bular foydalanuvchidan kelayotgan signal, yoki internetdan yuklanayotgan malumot, yoki malumotlar bazasida bajarilayotgan amallardir. Bazida I/O bound dasturlar qabul qilayotgan malumotri protsessdan o'tib tayor bo'lishini ham kutish evaziga umumiy olganda judayam uzoq kutib qolishlari mumkin, misol uchun siz foydalanuvchidan nimadur qabul qilmoqchi bo'lib input so'radingiz, u nima kiritishini uzoq o'ylashi mumkin, yoki siz malumotlar bazasidan so'ragan so'rovingizni tayorlash uchun malumotlar bazasining o'zida ushbu malumotni tayorlash uchun turli operatsiyalar talab etilishi mumkin.
Taymer implementatsiya qilingan dasturga misol:
# 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)
Mening 4 potokdan iborat bo'lgan protsessorimda bu code 1.902301549911499 sekund vaqt oldi
endi bizor kodga o'zgaritirish kiritib shu countdownni ikkita potokga ajratib beramiz:
# 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)
Ko'rib turganingizdek ikkala misol ham deyarli bir xil vaqt oldi, sababi biz ikkinchi kodda ikkita potokga ajratgan bo'lsakda GIL ikkita potokni paraller bajarilishiga yo'l qo'ymadi.
I/O bound tizimlarda ko'p potokli dasturlarga GIL unchalik sezilarli tasir ko'rsatmaydi, potoklar I/O ni kutish vaqtida locklarni o'zaro almashadilar.
Lekin potoklar to'liq CPU bound bo'lgan dasturlarda, misol uchun rasmlarni bo'laklarga bo'lib ishlov beradigan potoklar, lock evaziga nafaqat bir potokli bolib qoladi, balki unga ketadigan vaqtda ham o'sish ko'rishingiz mumkin, yuqoridagi misolga o'xshab, to'liq bitta potokdan foydalanilgan xolatga o'xshab.
Bu vaqtdagi o'sish potoklardagi locklarni ochish va lock qo'yish evaziga qo'shiladi.
Nega GIL hamon olib tashlanmagan ?
Ko'plab dasturchilar anchadan buyot norozilik bilan GIL ni olib tashlash kerakligi bilan murojaat qiladilar. Lekin Python kabi ko'p qo'llaniladigan dasturlash tiliga GIL ni olib tashlashchalik katta o'zgarishni backward compatible likka tasir qilmasdan amalga oshirishning imkoni yo'q.
Aslida Pythonning boshlanish fazalarida GIL bir necha marta Python contrubutorlari tomonidan o'chirib tashlangan, lekin bu urunishlar Pythonni ko'tarib turuvchi va GIL taqdim etayotgan xususiyatgan to'g'ridan to'g'ri bog'liq bo'lgan C extentionlarinign ishlay olmasligiga olib kelgan.
Va albatta GILning o'rniga ishlatishimiz mukin bo'lgan boshqa yechimlarning bazilari I/O bound tizimlarda sizning ko'p potokli hamda bir potokli Python dasturingiz ham bir varakayiga sezilarli darajada sekinlashishiga olib kelsa, bazilarini Pythonda implement qilish juda ham qiyin. Xullas Pythonning yangi versiyasini o'rnatishingiz dasturlaringizni sekin ishlashiga xizmat qilishini xoxlamasangiz kerak )).
Python BDFLi Guido Van Rossum Python comyunitiyga yuzlanib "GIL ni olib tashlash oson emas" degan edi.
Men Py3k (Pythonning 3... versialari) ga istalgan I/O bound tizimlarda Pythonning tezligiga tasir qilmaydigan o'zgarishlarni mamnunlik bilan qabul qilaman
[Guido Van Rossum]
GIL ni olib tashlashdagi harakatlarda ushbu shart hozirgacha qanoatlantirilgani yo'q.
Nima uchun GIL Python3 da ham o'chirilmadi ?
GIL ning Pythondan o'chirilishi Python3 ni Python2 dan ko'ra sekin qilib qo'yar edi. Hozirgi Python3 ning bir potokli lekin unumdorlik jhatdan kuchliligiga hechkim etiroz bildira olmaydi. Shunday ekan Pythonda hali hamon GIL mavjud.
Lekin Python3 da GIL ga ham bir qator o'zgartirishlar kiritilgan.
Biz yuqorida GIL ning CPU bound hamda I/O bound ko'p potokli dasturlarga tasirini o'rgandik. Lekin bir qismi CPU bound, bir qisim I/O bound dasturlarchi ?
Bunday dasturlarda GIL I/O bound potoklarni to'sib qoyadi, va shu tariqa ular CPU bound potoklarni olib qo'yishdan checklaydi.
Sababi Pythonda potokdan GIL malum vaqt intervalida ochib yuborilishi belgilangan, agar GIL ni boshqa potok talab qilmasagina avalgi potok yana o'z ishini davom ettirishi mumkin.
>>> import sys
>>> # The interval is set to 100 instructions:
>>> sys.getcheckinterval()
100
Bu usulning muammosi shundaki ko'pincha CPU bound potok boshqa potoklar GILni olmasdan oldin u qaytadan olib qo'yadi.
Bu muammo Python3.2 da 2009 yil Antoine Pitrou tomonidan hal qilingan.
Qanday qilib Python GIL ni chetlab o'tish mumkin
Ko'p-protsesslik - ko'p potoklilik: Ko'p protsesslikni eng qulay ishlatiladigan vaqti bu ko'p potoklilikning o'rniga ko'p protsesslar hosil qilib ishlatishdir. Shunda Python dastur har bir protsess uchun alohida Python interpreter hosil qiladi va shunda GIL bu yerda muammo bo'la olmaydi. Ushbu xususiyatni implement qilish uchun Pythonda multiprocessing moduli mavjud:
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)
Mening tizimimda ushbu dastur quyidagi vaqtda bajarildi:
Ko'p potokga ajratib tekshirgan dasturimizga nisbatan o'sish bo'ldi.
Nega Ketadigan vaqt 2 barobar kamayib ketmadi, sababi ko'p protsesslilikni ham boshqarishni o'ziga yarasha qiyin vaqt oladigan jihatlari bor.
Alternativ Python interpretatorlari: Pythonda ko'plab interpretator turlari bor. Cpython, JPython, IronPython and PyPy, written in C, Java, C# and Python lar eng ko'p qo'llaniladiganlaridur. GIL faqatgina Pythonning asl implementatsiyasida bor qaysiki C da yozilgan.
....