Race Condition چیست؟
به تکه کد زیر توجه کنید.
from threading import Thread
def add():
global number
for i in range(100000):
number += 1
def subtract():
global number
for i in range(100000):
number -= 1
number = 0
Thread(target=add).start()
Thread(target=subtract).start()
print(number)در شرایط ایدهآل، انتظار میرود خروجی کد بالا همواره برابر صفر باشد. اما اگر این کد را به دفعات زیادی اجرا کنید متوجه خواهید شد که گاهی خروجی عددی به جز صفر است. به این مشکل، Race Condition یا شرایط رقابتی میگویند.
Shared Resource چیست؟
به number که در کد بالا به صورت مشترک بین دو ترد استفاده شده است، Shared Resource یا منابع اشتراکی میگویند.
علت رخداد Race Condition
مثال – آشپز که دو تا بشه …
دو آشپز میخواهند یک آش بپزند. دیگی میآورند. هر کدام موادی را که به نظرشان لازم است به دیگ اضافه میکنند. در انتها احتمال دارد آش یا شور شده باشد یا بی نمک. در این مثال:
- آشپزها نماد thread ها هستند.
- دیگ نماد منبع اشتراکی یا shared resource است.
- آش نماد خروجی برنامه است.
وجود دو آشپز برای پخت آش لزوما بد نیست. مشکل این است که این دو آشپز با هم هماهنگ نبودند. به همین دلیل خروجی کار با چیزی که انتظار میرفت متفاوت بود.
از این مثال متوجه میشویم که دلیل Race Condition عدم هماهنگی ترد ها در استفاده از منابع مشترک بوده است.
مثال – افزایش و کاهش شمارنده
بیایید بار دیگر مثالی که در ابتدای این بخش ذکر شد را دقیق تر بررسی کنیم.
from threading import Thread
def add():
global number
for i in range(100000):
number += 1
def subtract():
global number
for i in range(100000):
number -= 1
number = 0
Thread(target=add).start()
Thread(target=subtract).start()
print(number)در این کد، تابع add برای افزایش و تابع subtract برای کاهش مقدار number طراحی شدهاند. همانطور که میبینید با استفاده از دستور global این counter بین دو تابع به صورت مشترک قابل دسترس است. اگر این کد را میخواستیم در پارادایم Sequential Programming اجرا کنیم، مشکلی نداشتیم زیرا اول تابع add مقدار آن را افزایش و سپس تابع subtract مقدار آن را کاهش میداد و در نهایت همیشه مطمئن بودیم که خروجی برنامه برابر صفر خواهد بود.
اما این کد با استفاده از ماژول threading توسعه یافته و این یعنی تابع add و subtract هر دو میتوانند به صورت همزمان در یک لحظه به مقدار number دسترسی داشته باشند.
در چنین شرایطی اگر هر دو اقدام به تغییر مقدار آن بکنند مشکل Race Condition ایجاد میشود. برای درک بهتر از این موضوع به فیلم زیر توجه کنید.
شرح ویدئو بالا
مرحله ۱
یک ترد برای عملیات add و یک ترد برای عملیات subtract ساخته میشود. همچنین counter به عنوان یک منبع اشتراکی با مقدار اولیه صفر مقدار دهی میشود.
مرحله ۲
در این مرحله هر یک از thread ها مقدار counter را میخوانند تا محاسبات شان را با آن انجام دهند.
مرحله ۳
در این مرحله مقدار counter در thread ها قرار گرفته اما هنوز محاسبه ای با آن مقدار انجام نشده است.
مرحله ۴
در این مرحله add یک واحد به counter ای که در خود نگه داشته اضافه میکند. توجه داشته باشید که در این مرحله مقدار counter در subtract هنوز برابر صفر است.
مرحله ۵
مقدار منبع اشتراکی با مقداری که در add قرار دارد بهروز میشود.
مرحله ۶
در این مرحله صرفا میبینیم که مقدار counter به عنوان یک منبع اشتراکی برابر مقداری شده که add محاسبه کرده بود. اما مقدار counter در subtract هنوز برابر صفر است.
مرحله ۷
در این مرحله subtract یک واحد از counter ای که از قبل خوانده کم میکند و مقدار counter از نظر subtract برابر -۱ میشود.
مرحله ۸
در این مرحله subtract مقداری که برای counter در نظر گرفته را در منبع اشتراکی قرار داده و مقدار counter به را برابر -۱ قرار میدهد.
مرحله ۹
در این مرحله میبینید که خطای Race Condition رخ داده است. در این مرحله انتظار داریم که مقدار counter به عنوان منبع اشتراکی برابر صفر باشد اما برابر -۱ است. چرا؟ چون که در حالی که subtract در حال محاسبه مقدار جدید منبع اشتراکی بود، add محاسباتش را انجام داد و مقدار counter را آپدیت کرد و subtract هنوز نمیدانست که مقدار counter به روز شده است. پس با همان مقدار قدیمی که از counter داشت محاسباتش را انجام داد و مقدار counter را با مقدار اشتباهی جایگزین کرد. این داستان ادامه مییابد و بارها و بارها توسط add و subtract تکرار خواهد شد تا اینکه برنامه تمام شود و یک مقدار کاملا بیربط از counter محاسبه شود.
جلوگیری از Race Condition
مثال – سرآشپزی که آشپز ها رو مدیریت کنه …
دو آشپز مثال قبل تصمیم گرفتند از یک سرآشپز کمک بگیرند. آنها از سرآشپز خواستند که کار ها را به نحوی مدیریت کند که دیگر آش شور یا بینمک نشود. سرآشپز هم با دقت مدیریت آشپزخانه را تحت نظر گرفت. هرگاه آشپز اول نمک میریخت به خاطر میسپارد و اگر آشپز دوم میخواست دوباره نمک بریزد، مانع اش میشد. در این مثال، سرآشپز نماد قفل کننده یا locker است.
چطور از Race Condition جلوگیری کنیم؟
زمانی که ترد ها همزمان از منابع مشترک استفاده کنند Race Condition رخ میدهد. برای خل این مشکل کافیست از استفاده همزمان از منابع مشترک جلوگیری کنیم. این کار با قفل کردن منابع مشترک قابل انجام است. به این عمل lock کردن نیز گفته میشود.
در پایتون سه ماژول threading و concurrent.future و _thread برای برنامه نویسی multithread وجود دارند که هر کدام روش قفل گذاری خاص خود را دارند. این روش ها را به تفصیل در بخش قفل کردن منابع مشترک در Multithreaded Programming شرح داده ام.
جلوگیری از Race Condition در ماژول _thread
کد زیر را در نظر بگیرید. این کد با ماژول _thread نوشته شده و خطای Race Condition دارد.
import time
import _thread as thread
# شمارنده مشترک
counter = 0
def add(thread_id):
global counter
for _ in range(100):
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):
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 کد بالا را به شکل زیر تغییر میدهیم.
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}")توضیح کد بالا:
خط ۸: در این خط یک قفل ایجاد کردهایم.
خط ۱۵: در این خط مشخص کردهایم که هر وقت عملیات add میخواست انجام شود، منبع اشتراکی counter برای استفاده در add باید قفل شود.
خط ۲۲: در این خط مشخص کردهایم که هر وقت عملیات add میخواست پایان یابد، منبع اشتراکی counter برای استفاده سایر ترد ها باید آزاد شود.
خط ۲۸: در این خط مشخص کردهایم که هر وقت عملیات subtract میخواست انجام شود، منبع اشتراکی counter برای استفاده از subtract باید قفل شود.
خط ۳۵: در این خط مشخص کردهایم که هر وقت عملیات subtract میخواست پایان یابد، منبع اشتراکی counter برای استفاده سایر ترد ها باید آزاد شود.
البته برای سهولت بیشتر و افزایش قابلیت اطمینان برنامه، میتوان به جای استفاده مستقیم از acquire و release از دستور with استفاده کرد. در ادامه کد بالا را مشاهده میکنید که با with بازنویسی شده است.
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 در threading.Thread
کد زیر را در نظر بگیرید. این کد با استفاده از threading.Thread توسعه یافته است.
from threading import Thread
def add():
global number
for i in range(100000):
number += 1
def subtract():
global number
for i in range(100000):
number -= 1
number = 0
Thread(target=add).start()
Thread(target=subtract).start()
print(number)برای حل مشکل Race Condition در کد بالا دو راه داریم:
- استفاده از
lockبه همراهacquireوrelease - استفاده از
lockبه همراهwith
حل مشکل Race Condition با lock و acquire و release
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)حل مشکل Race Condition با lock و with
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)نکات مهم در مورد Lock و Release
در زمانی که منبع اشتراکی lock است و release نشده، thread های دیگر باید منتظر بمانند تا آن منبع اشتراکی توسط thread ای که lock شده مجدد release شود.
به کدی که کدی که مشکل Race Condition ندارد thread safe میگویند.
ت