רמאויות של תכנות מרובה תהליכים בפייתון
על פניו נדמה ש Python עם ה GIL חוסכת לנו את הצרות של סינכרון בין תהליכונים שרצים במקביל: הרי כל העניין של ה GIL שהוא מונע משני תהליכים לבצע פעולות במקביל באותו Python Interpreter. ובכל זאת המודול threading כולל מנעולים, ומסיבה טובה.
נתבונן בקוד הבא:
class Counter:
def __init__(self, total):
self.count = total
def take(self, n=1):
val = self.count
self.count = val - n
print(f"Remaining: {self.count}")
יש לכם רעיון איך לגרום למונה לטעות בספירה?
1. שימוש לא נכון במונה
המפתח נמצא כמובן בהרצת המונה מכמה תהליכונים במקביל. בהיעדר נעילה קל לדמיין מצב בו תהליכון אחד טוען את הערך מ self.count למשתנה val ובדיוק אז השליטה עוברת לתהליכון שני שיבצע את take
במלואה ויקטין את self.count ב-1 (או בכמה שצריך). כשהתהליכון הראשון ימשיך את take
הערך שיש לו ביד במשתנה val
הוא הערך לפני העדכון ולכן העדכון של התהליכון הראשון ידרוס את העדכון שכבר בוצע על ידי התהליכון השני.
בתוכניות דוגמא קטנות לא כל כך קל לגרום להתנגשויות מסוג זה ולכן הייתי צריך עדכון קל של קוד המונה ותוספת של פקודת sleep במקום אסטרטגי. זו תוכנית מלאה שכבר מציגה את ההתנגשות ולמרות ש take מופעלת 100 פעמים התוכנית לא מדפיסה אפס:
from threading import Thread
import time
class Counter:
def __init__(self, total):
self.count = total
def take(self, n=1):
val = self.count
time.sleep(0)
self.count = val - n
print(f"Remaining: {self.count}")
class Consumer(Thread):
def __init__(self, count, counter):
super().__init__()
self.count = count
self.counter = counter
def run(self):
for i in range(self.count):
self.counter.take()
self.count -= 1
c = Counter(100)
consumers = []
for i in range(10):
consumers.append(Consumer(10, c))
consumers[-1].start()
[t.join() for t in consumers]
print(c.count)
קל לטעות ולחשוב שאם נבטל את ה sleep מצבנו ישתפר. האמת היא שגם אם נשנה את take למימוש הכי קומפקטי שאפשר היא עדיין תטעה במקרים מספיק מעניינים:
# still wrong...
def take(self, n=1):
self.count -= n
print(f"Remaining: {self.count}")
2. תיקון באמצעות מנעולים
זוכרים את המנעול מהמודול threading? מסתבר שהוא לא בטעות שם. בעזרתו נוכל לבנות מונה הרבה יותר מדויק. התוכנית הבאה כבר מתמודדת יפה עם התנגשויות מכל הסוגים:
from threading import Thread, Lock
import time
class Counter:
def __init__(self, total):
self.count = total
self.lock = Lock()
def take(self, n=1):
with self.lock:
val = self.count
time.sleep(0)
self.count = val - n
print(f"Remaining: {self.count}")
class Consumer(Thread):
def __init__(self, count, counter):
super().__init__()
self.count = count
self.counter = counter
def run(self):
for i in range(self.count):
self.counter.take()
self.count -= 1
c = Counter(100)
consumers = []
for i in range(10):
consumers.append(Consumer(10, c))
consumers[-1].start()
[t.join() for t in consumers]
print(c.count)
שווה לציין שנעילות מאטות את הקוד שלכם ולכן אם אפשר מומלץ לחשוב על פתרונות יצירתיים שלא יחייבו נעילות. וכן לפעמים כדאי לשקול לוותר על תצוגת ה"נותרים".