چرا باید قفل گذاری کنیم؟
منابع یک process بین thread هایش به اشتراک گذاشته میشود. در صورتی ترد ها بخواهند در یک لحظه از یک منابع مشترک استفاده کنند مشکلاتی نظیر Race Condition یا خطای مقابله ای رخ میدهد. با قفلگذاری میتوان مشخص کرد که یک منبع اشتراکی در هر لحظه توسط کدام ترد استفاده شود.
به عملیات قفل کردن منابع مشترک، Lock کردن هم گفته میشود.
مثال – سرویس بهداشتی عمومی
استفاده از یک سرویس بهداشتی عمومی که درب آن قفل ندارد چه حسی دارد؟ هر لحظه ممکن است آن اتفاقی که نباید، رخ دهد. یک منبع مشترک مانند یک سرویس بهداشتی عمومی است و افرادی که میخواهند از آن استفاده کنند مانند ترد هایی هستند که از آن منبع مشترک استفاده میکنند.
همانطور که برای حل مشکل سرویس بهداشتی کافیست برای درب آن قفل بگذاریم و آن را قفل کنیم، در مولتی تردینگ نیز برای حل این مشکل از عملیات قفل گذاری استفاده میکنیم. به عملیات قفلگذاری در اصطلاح فنیتر، lock کردن گفته میشود.برای برنامهنویسی مولتی ترد در پایتون سه ماژول زیر وجود دارد:
قفل گذاری در ماژول _thread
قفل گذاری شامل سه مرحله است:
- اول: ایجاد قفل
- دوم: بستن قفل
- سوم: باز کردن قفل
import _thread as thread
import time
# شمارنده مشترک
counter = 0
# قفل برای همگامسازی دسترسی به متغیر مشترک
lock = thread.allocate_lock()
# تابع افزایش شمارنده
def add(thread_id):
global counter
for _ in range(100):
# قفل گرفتن برای دسترسی به متغیر مشترک
lock.acquire()
temp = counter
temp += 1
time.sleep(0.01) # اضافه کردن یک خواب کوتاه برای تشدید شرایط رقابتی
counter = temp
print(f"Thread {thread_id}: {counter}")
# قفل رها کردن بعد از استفاده
lock.release()
def subtract(thread_id):
global counter
for _ in range(100):
# قفل گرفتن برای دسترسی به متغیر مشترک
lock.acquire()
temp = counter
temp -= 1
time.sleep(0.01) # اضافه کردن یک خواب کوتاه برای تشدید شرایط رقابتی
counter = temp
print(f"Thread {thread_id}: {counter}")
# قفل رها کردن بعد از استفاده
lock.release()
# ایجاد چندین نخ
thread.start_new_thread(add, (1,))
thread.start_new_thread(subtract, (2,))
# منتظر ماندن برای پایان نخها
time.sleep(5)
print(f"Final counter value: {counter}")در تکه کد بالا:
- خط ۸: با استفاده از
thread.allocate_lockقفل ساخته میشود. - خط ۱۵ و ۲۸: با استفاده از
acquireقفل بسته میشود. - خط ۲۲ و ۳۵: با استفاده از
releaseقفل باز میشود.
اگر یک ترد، هر کدام از acquire ها را اجرا کند، ترد های دیگر زمانی که به acquire ها برسند متوقف میشوند تا آن ترد با اجرای release، قفل را باز و منابع مشترک را آزاد کند.
بدین ترتیب تمام کد هایی که بین acquire و release نوشته میشوند در یک لحظه فقط توسط یک ترد اجرا میشوند.
استفاده از with برای قفل کردن
به جای استفاده از acquire و release میتوان از دستور with برای قفل کردن استفاده کرد. این کار علاوه بر افزایش خوانایی کد، موجب کاهش احتمال بروز خطای Dead Lock میشود.
import time
import _thread as thread
# شمارنده مشترک
counter = 0
# قفل برای همگامسازی دسترسی به متغیر مشترک
lock = thread.allocate_lock()
# تابع افزایش شمارنده
def add(thread_id):
global counter
for _ in range(100):
# قفل گرفتن برای دسترسی به متغیر مشترک با استفاده از with
with lock:
temp = counter
temp += 1
time.sleep(0.01) # اضافه کردن یک خواب کوتاه برای تشدید شرایط رقابتی
counter = temp
print(f"Thread {thread_id}: {counter}")
# تابع کاهش شمارنده
def subtract(thread_id):
global counter
for _ in range(100):
# قفل گرفتن برای دسترسی به متغیر مشترک با استفاده از with
with lock:
temp = counter
temp -= 1
time.sleep(0.01) # اضافه کردن یک خواب کوتاه برای تشدید شرایط رقابتی
counter = temp
print(f"Thread {thread_id}: {counter}")
# ایجاد چندین نخ
thread.start_new_thread(add, (1,))
thread.start_new_thread(subtract, (2,))
# منتظر ماندن برای پایان نخها
time.sleep(5)
print(f"Final counter value: {counter}")خلاصه
- هنگامی که منابع مشترک بین ترد ها وجود دارد، برای جلوگیری از رخ دادن مشکلات الگوریتمی ناشی از Race Condition، ترد ها را قفل میکنیم.
- برای افزایش خوانایی کد و کاهش خطای Dead Lock استفاده از
withبه جایacquireوreleaseپیشنهاد میشود.