پیشنیاز ها
برای درک بهتر از مطالب این بخش، لازم است با مفاهیم قفل گذاری در مولتی تردینگ آشنا باشید.
کلاس Semaphore
با استفاده از کلاس Semaphore میتوان تعداد حداکثر ترد هایی که در یک لحظه میتوانند به صورت مشترک به منابع اشتراکی دسترسی داشته باشند را مشخص کرد.
تعداد ترد هایی که همزمان میتوانند به منبع اشتراکی دسترسی داشته باشند با یک شمارنده مدیریت میشود. فراخوانی acquire یک واحد از آن کم و فراخوانی release یک واحد به آن اضافه میکند. به عبارت دیگر:
- ابتدا یک قفل ساخته میشود.
- هر بار که
acquireفراخوانی شود یکی از آن شمارنده کم میشود. - هر بار که
releaseفراخوانی شود یکی به آن شمارنده اضافه میشود.
فرض کنید بخواهیم در هر لحظه حداکثر دو ترد به صورت همزمان بتوانند به منابع مشترک دسترسی داشته باشند. بدین منظور باید کلاس Semaphore را به شکل زیر به کار بگیریم.
from time import sleep
from threading import Thread, Semaphore, current_thread
num = 0
lock = Semaphore(2)
def add():
lock.acquire()
# Access to common resources
print('Downloading image by', current_thread().name)
sleep(1)
lock.release()
t1 = Thread(target=add)
t2 = Thread(target=add)
t3 = Thread(target=add)
t4 = Thread(target=add)
t5 = Thread(target=add)
t1.start()
t2.start()
t3.start()
t4.start()
t5.start()مشکل عملکرد Semaphore
برنامه هایی که در دنیای واقعی نوشته میشوند همیشه به سادگی مثال های دوره های آموزشی نیست. گاهی ممکن است کد ها پیچیده شوند و ناخواسته الگوریتم برنامه به نحوی باشد که تعداد فراخوانی های release بیشتر از acquire باشد. در اینصورت عملکرد Semaphore خراب میشود.
مثلا در کد زیر، به ازای هر بار acquire دو بار release فراخوانی میشود.
from time import sleep
from threading import Thread, Semaphore, current_thread
num = 0
lock = Semaphore(2)
def add():
lock.acquire()
print('Downloading image by', current_thread().name)
sleep(2)
lock.release()
lock.release()
threads = []
for i in range(13):
threads.append(Thread(target=add))
for t in threads:
t.start()
مشکل کد بالا این است که به مرور زمان، تعداد ترد هایی که به صورت همزمان به منابع مشترک دسترسی پیدا میکنند به مرور بیشتر میشود.
کلاس BoundedSemaphore
برای جلوگیری از مشکلی که برای کلاس Semaphore مطرح شد، میتوان از کلاس BoundedSemaphore استفاده کرد.
هر بار که release بیش از اندازه فراخوانی شود، یک ValueError رخ میدهد.
from time import sleep
from threading import Thread, BoundedSemaphore, current_thread
num = 0
lock = BoundedSemaphore(2)
def add():
try:
lock.acquire()
print('Downloading image by', current_thread().name)
sleep(2)
lock.release()
lock.release()
except ValueError:
print('Can not release', current_thread().name)
threads = []
for i in range(13):
threads.append(Thread(target=add))
for t in threads:
t.start()مقایسه Semaphore و BoundedSemaphore
از آنجایی که BoundedSemaphore علاوه بر داشتن امکانات Semaphore، مشکلات آن را هم ندارد، احتمالا این سوال به ذهن شما خطور کرده باشد که استفاده از Semaphore در چه مواردی میتواند کاربرد داشته باشد؟
کلاس BoundedSemaphore پس از Semaphore توسعه یافته است. به همین دلیل ممکن است برخی کد های قدیمی وجود داشته باشد که با Semaphore توسعه یافته باشند. نگه داشتن Semaphore تنها کمکی که میکند این است که آن کد های قدیمی، کمتر نیاز به ریفکتور و هماهنگی با نسخه های جدید پایتون داشته باشند. پس میتوان گفت، تنها دلیل حذف نشدن Semaphore در واقع Backward Compatibility است.
خلاصه
همیشه برای ایجاد محدودیت تعداد ترد ها از BoundedSemaphore استفاده کنید.