logo

מה זה GIL ב-Python? מנעול מתורגמן גלובלי

מדריך זה יתמקד באחד מהנושאים החשובים של Python, GIL. נסקור גם כיצד ה-GIL משפיע על הביצועים של תוכניות Python עם יישום הקוד. לפני שנצלול לנושא זה, בואו נקבל מושג בסיסי על ה-GIL.

GIL או Global Interpreter Lock

Python Global Interpreter Lock או GIL הוא חלק חשוב בתכנות ריבוי השחלות. זהו סוג של נעילת תהליך המשמש כאשר עובדים עם מספר תהליכים. זה נותן את השליטה לחוט אחד בלבד. בדרך כלל, Python משתמש בשרשור בודד כדי להפעיל תהליך בודד. אנו מקבלים את אותה תוצאת ביצועים של תהליכים חד-הברגים ותהליכים מרובי-הברגים באמצעות ה-GIL. זה מגביל את השגת ריבוי השרשורים ב- Python מכיוון שהוא מונע את השרשורים ועובד כחוט יחיד.

הערה - Python לא תומך בריבוי הליכי שרשור מכיוון שחבילות השרשור לא יכלו לאפשר לנו להשתמש בליבות המעבד המרובות.

מדוע מפתחי Python משתמשים ב-GIL?

Python מספקת את תכונת מונה ההתייחסות הייחודית, המשמשת לניהול זיכרון. מונה הפניות סופר את המספר הכולל של הפניות שבוצעו באופן פנימי ב- Python כדי להקצות ערך לאובייקט נתונים. כאשר ספירת ההתייחסות מגיעה לאפס, הזיכרון המוקצה של האובייקט משתחרר. בוא נראה את הדוגמה למטה.

דוגמא -

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

החשש העיקרי עם משתנה ספירת ההתייחסות הוא שהוא יכול להיות מושפע כאשר שניים או שלושה שרשורים מנסים להגדיל או להקטין את ערכו בו זמנית. זה ידוע כמצב גזע. אם מצב זה מתרחש, זה יכול להיגרם זיכרון דלף שלעולם לא משתחרר. זה עלול לקרוס או באגים בתוכנית Python.

רשת numpy

GIL עוזרת לנו להסיר מצב כזה על ידי שימוש במנעולים לכל מבני הנתונים המשותפים על פני שרשורים, כך שהם לא ישתנו באופן לא עקבי. Python מספקת דרך קלה ליישם את ה-GIL שכן היא עוסקת בניהול זיכרון בטוח. GIL דורש להציע נעילה יחידה לשרשור לעיבוד ב- Python. זה מגדיל את הביצועים של תוכנית עם חוט יחיד, מכיוון שרק נעילה אחת דורשת טיפול. זה גם עוזר ליצור כל תוכנית הקשורה למעבד ומונע את מצב הקיפאון.

ההשפעה על תוכניות פייתון מרובי הליכי

יש הבדל בין גבולות ה-CPU בביצועים שלהם לבין I/O הקשורים לתוכנית Python טיפוסית או כל תוכנת מחשב. תוכניות הקשורות למעבד בדרך כלל דוחפות את המעבד לגבולותיו. תוכניות אלו משמשות בדרך כלל לחישוב מתמטי כגון כפל מטריצות, צריבה, עיבוד תמונה וכו'.

תוכניות קשורות ל-I/O הן אותן תוכניות שמבזבזות זמן כדי לקבל קלט/פלט שיכול להיווצר על ידי המשתמש, הקובץ, מסד הנתונים, הרשת וכו'. תוכניות כאלה צריכות לחכות פרק זמן משמעותי עד שהמקור מספק את הקלט. מצד שני, למקור יש גם זמן עיבוד משלו. לדוגמה - משתמש חושב מה להזין כקלט.

בואו נבין את הדוגמה הבאה.

int ל-Java מחרוזת

דוגמא -

 import time from threading import Thread COUNT = 100000000 def countdown(num): while num>0: num -= 1 start_time = time.time() countdown(COUNT) end_time = time.time() print('Time taken in seconds -', end_time - start_time) 

תְפוּקָה:

 Time taken in seconds - 7.422671556472778 

כעת אנו משנים את הקוד לעיל על ידי הפעלת שני השרשורים.

דוגמה - 2:

 import time from threading import Thread COUNT = 100000000 def countdown(num): while num>0: num -= 1 thread1 = Thread(target=countdown, args=(COUNT//2,)) thread2 = Thread(target=countdown, args=(COUNT//2,)) start_time = time.time() thread1.start() thread2.start() thread1.join() thread2.join() end_time = time.time() print('Time taken in seconds -', end_time - start_time) 

תְפוּקָה:

 Time taken in seconds - 6.90830135345459 

כפי שאנו יכולים לראות שלשני הקודים לקח אותו זמן לסיום. GIL מנע מהשרשורים הקשורים למעבד לפעול במקביל בקוד השני.

מדוע ה-GIL עדיין לא הוסר?

למתכנתים רבים יש תלונה לגבי זה, אבל Python לא יכול להביא את השינויים משמעותיים כמו הסרת GIL. סיבה נוספת היא ש-GIL אינו משופר נכון לעכשיו. אם זה ישתנה ב- Python 3, זה יצור כמה בעיות רציניות. במקום להסיר את GIL, הרעיון של GIL יכול להשתפר. לפי גידו ואן רוסום -

'אשמח על קבוצת תיקונים ל-Py3k רק אם הביצועים של תוכנית עם הליכי פתיל בודדים (ועבור תכנית מרובת פתקים אך קשורה ל-I/O) אינם יורדים'.

אובייקט ב-java

יש גם הרבה שיטות זמינות שפותרות את אותה בעיה שנפתרה על ידי ה-GIL, אבל יש קשות ליישום.

כיצד להתמודד עם GIL של Python

שימוש ב-multiprocessing הוא הדרך המתאימה ביותר למנוע מהתוכנית מ-GIL. Python מציעה מתורגמנים שונים להפעלת כל תהליך, כך שבתרחיש זה, החוט היחיד מסופק לכל תהליך בריבוי עיבודים. בואו נבין את הדוגמה הבאה.

דוגמא -

 from multiprocessing import Pool import time COUNT = 50000000 def countdown(num): while num>0: num -= 1 if __name__ == '__main__': pool = Pool(processes=2) start_time = time.time() r1 = pool.apply_async(countdown, [COUNT//2]) r2 = pool.apply_async(countdown, [COUNT//2]) pool.close() pool.join() end_time = time.time() print('Time taken in seconds -', end_time - start_time) 

תְפוּקָה:

 Time taken in seconds - 3.3707828521728516 

אולי נראה שהביצועים הגונים גדלים, אבל לניהול תהליכים יש תקורה משלו ותהליכים מרובים כבדים יותר ממספר חוטים.

סיכום

במדריך זה, דנו ב-GIL וכיצד אנו יכולים להשתמש בו. זה נותן את השליטה לשרשור בודד לביצוע בזמן. הדרכה זו סיקרה גם מדוע GIL חשוב למתכנתי Python.