پیشنیاز
قبل از مطالعه این بخش، باید با مفهوم قفل کردن منابع مشترک و چرایی آن آشنا باشید.
قفل گذاری در threading.Thread
قفلگذاری در threading.Thread کاملا مشابه قفل کردن منابع مشترک در ماژول _thread در ۳ مرحله انجام میشود.
- ایجاد قفل
- بستن قفل
- باز کردن قفل
برای دیدن نحوه انجام سه مرحله فوق، به تکه کد زیر توجه کنید.
from threading import Thread, Lock
# تعریف قفل
lock = Lock()
def add():
global number
for i in range(100000):
# قفل کردن قبل از دسترسی به متغیر مشترک
lock.acquire()
number += 1
# آزاد کردن قفل بعد از دسترسی
lock.release()
def subtract():
global number
for i in range(100000):
# قفل کردن قبل از دسترسی به متغیر مشترک
lock.acquire()
number -= 1
# آزاد کردن قفل بعد از دسترسی
lock.release()
number = 0
# ایجاد و شروع رشتهها
thread1 = Thread(target=add)
thread2 = Thread(target=subtract)
thread1.start()
thread2.start()
# منتظر ماندن برای اتمام رشتهها
thread1.join()
thread2.join()
# چاپ مقدار نهایی number
print(number)- خط ۴: ایجاد قفل – ابتدا با ساخت یک instance از کلاس
Lock، یک قفل میسازیم. - خط ۱۰ و ۱۹: بستن قفل – سپس با فراخوانی متد
acquireقبل از استفاده از منبع مشترک، آن را قفل میکنیم. اگر قبلا، ترد دیگری آن را قفل کرده باشد، ترد در این قسمت متوقف شده و آنقدر منتظر میماند تا ترد دیگر قفل را باز کند. - خط ۱۳ و ۲۲: باز کردن قفل – در نهایت، زمانی که کارمان با منبع مشترک تمام شد، با فراخوانی متد
release، آن منبع مشترک را آزاد میکنیم.
هنگام استفاده از کلاس Lock برای قفل کردن ممکن است خطای Dead Lock رخ دهد. برای کاهش احتمال بروز خطا، اقدامات زیر را انجام دهید.
در ادامه به شرح هر یک میپردازیم.
استفاده از Rlock برای قفل گذاری
Rlock (Recursive Lock) بیشتر مناسب سناریو هایی است که ترد ها با الگویی بازگشتی توسعه داده شده باشند. با استفاده از Rlock، میتوان بارها و بارها بدون نگرانی از اینکه خطای Dead Lock رخ دهد، متد acquire را فراخوانی کرد.
مثال – اتاق سرور بانک مرکزی
فرض کنید میخواهید وارد اتاق سرور بانک مرکزی شوید. سرورها در اتاق A قرار دارند. اتاق A در اتاق B و اتاق B نیز در اتاق C قرار دارد. برای وارد شدن به هر اتاق باید درب اتاق قبلی را پشت سر خود قفل کنید. در این زمان که شما داخل هستید شخص دیگری نمیتواند درب را باز کرده و وارد شود. همچنین وقتی میخواهید از اتاقها خارج شوید باید قفل ها را یکی پس از دیگری باز کنید تا نهایتا از اتاقها خارج شوید.
اگر برای قفل گذاری از Lock استفاده کرده باشید، هنگاهی که یک ترد به acquire برسد و قبلا عملیات قفل گذاری انجام شده باشد، برنامه متوقف شده و دچار خطای Dead Lock میشود اما اگر از RLock استفاده شده باشد، برنامه بدون توقف ادامه یافته و صرفا یک قفل روی قفل قبلی گذاشته میشود.
میتوان اینطور در نظر گرفت که هنگام استفاده از Rlock تعداد دفعات فراخوانی acquire در یک شمارنده ذخیره میشود و در نهایت به همان تعداد باید متد release را فراخوانی کنیم.
در این مثال فردی که وارد اتاقها میشود مانند ترد و قفل کردن درب هر اتاق مانند قفل کردن منابع مشترک است. هر تعداد بار که وارد اتاق شده و درب را قفل کنید، برای خارج شدن از آنها نیز به همان تعداد باید قفلها را باز کنید.
import threading
# ایجاد یک قفل بازیابی
rlock = threading.RLock()
def rfunc(n):
if n <= 0:
return
rlock.acquire()
try:
print(f"Thread {threading.current_thread().name} acquired lock with n = {n}")
rfunc(n - 1)
print(f"Thread {threading.current_thread().name} released lock with n = {n}")
finally:
rlock.release()
# ایجاد و شروع نخها
threads = []
for i in range(3):
t = threading.Thread(target=rfunc, args=(3,), name=f'Thread-{i}')
threads.append(t)
t.start()
# منتظر ماندن برای تمام نخها
for t in threads:
t.join()
print("All threads have finished execution.")- خط ۴: یک نمونه از کلاس
Rlockساختهایم. - خط ۶ تا ۱۵: یک تابع به نام
rfuncساختهایم که در خودش عملیات قفلگذاری را انجام داده و همچنین خودش را نیز به صورت بازگشتی در خط ۱۲ فراخوانی میکند.
استفاده از with برای قفل گذاری
به جای استفاده از acquire و release میتوان از دستور with برای قفل کردن استفاده کرد. این کار علاوه بر افزایش خوانایی کد، موجب کاهش احتمال بروز خطای Dead Lock میشود.
from threading import Thread, Lock
# تعریف قفل
lock = Lock()
def add():
global number
for i in range(100000):
# قفل کردن قبل از دسترسی به متغیر مشترک با استفاده از with
with lock:
number += 1
def subtract():
global number
for i in range(100000):
# قفل کردن قبل از دسترسی به متغیر مشترک با استفاده از with
with lock:
number -= 1
number = 0
# ایجاد و شروع رشتهها
thread1 = Thread(target=add)
thread2 = Thread(target=subtract)
thread1.start()
thread2.start()
# منتظر ماندن برای اتمام رشتهها
thread1.join()
thread2.join()
# چاپ مقدار نهایی number
print(number)خلاصه
- هنگامی که منابع مشترک بین ترد ها وجود دارد، برای جلوگیری از رخ دادن مشکلات الگوریتمی ناشی از Race Condition، ترد ها را قفل میکنیم.
- برای افزایش خوانایی کد و کاهش خطای Dead Lock استفاده از
withبه جایacquireوreleaseپیشنهاد میشود.