مقدمه
اگر دیدید یک برنامه مولتی ترد اجرا نمیشود و انگار مرده است، برنامه دچار مشکل Dead Lock شده است.
مثال – سرویس بهداشتی
فرض کنید وارد یک سرویس بهداشتی عمومی میشوید و میبینید که همه دربها بسته هستند. پس صبر میکنید تا یک نفر خارج شود. در این مدت یک نفر دیگر هم وارد سرویس بهداشتی میشود. او که شما را در حال انتظار میبیند نیز پشت سر شما میایستند و منتظر میشود تا پس از شما از سرویس استفاده کند. پس از چند لحظه، نگهبان سرویسبهداشتی وارد شده و برای نظافت دربها را باز میکند. اینجاست که شما دو نفر متوجه میشوید که همه سرویسهای بهداشتی خالی بوده اند و این مدت را بیهوده منتظر ماندهاید.
در این مثال، افرادی که در انتظار خالی شدن سرویس بهداشتی هستند، شبیه ترد هایی هستند که در انتظار آزاد شدن منبع اشتراکی هستند در صورتی که منبع اشتراکی آزاد بوده است.
در این مثال، نگهبان سرویس بهداشتی پس از مدتی آمد و نشان داد که سرویسهای بهداشتی خالی هستند و افراد توانستند از آنها استفاده کنند اما در برنامهنویسی این اتفاق نمیافتد و thread ها تا زمانی که برنامه در حال اجراست منتظر میمانند تا منبع اشتراکی آزاد شود.
الگو هایی با مشکل Dead Lock
در برنامه نویسی مولتی ترد، چندین الگو با مشکل Dead Lock وجود دارد. در ادامه مهم ترین الگو ها به همراه راه حل شان ارائه شده است.
سناریو اول – قفل کردن دوباره یک قفل (Self-Deadlock)
کد زیر را در نظر بگیرید. این کد مشکل dead lock دارد.
import threading
lock = threading.Lock()
def func1():
lock.acquire()
print("Function 1 acquired lock 1")
# تابع دوم داخل اینجا فراخوانی شود
func2()
lock.release()
def func2():
lock.acquire()
print("Function 2 acquired lock 2 inside Function 1")
lock.release()
# تعریف نخ برای اجرای func1
thread1 = threading.Thread(target=func1)
# شروع اجرای نخ
thread1.start()
# منتظر ماندن تا تمام نخها خاتمه یابند
thread1.join()
print("Finished")در کد بالا:
- خط ۶: قفل بسته میشود.
- خط ۹: تابع
func2فراخوانی میشود. - خط ۱۳: چون قبلا در خط ۶ قفل گذاشته شده،
acquireمنتظر میماند تا آن قفل باز شود. اینجاست که منتظر باز شدن قفلی میمانیم که هرگز باز نمیشود.
برای حل این مشکل، میتوان از دو قفل مجزا استفاده کرد.
import threading
lock1 = threading.Lock()
lock2 = threading.Lock()
def func1():
lock1.acquire()
print("Function 1 acquired lock 1")
# تابع دوم داخل اینجا فراخوانی شود
func2()
lock1.release()
def func2():
lock2.acquire()
print("Function 2 acquired lock 2 inside Function 1")
lock2.release()
# تعریف نخ برای اجرای func1
thread1 = threading.Thread(target=func1)
# شروع اجرای نخ
thread1.start()
# منتظر ماندن تا تمام نخها خاتمه یابند
thread1.join()
print("Finished")سناریو دوم – ترتیب اشتباه قفل ها
قبل از اینکه این الگو را در قالب کد بررسی کنیم به مثال زیر توجه کنید.
مثال – جا گذاشتن کلید
در یک روز تعطیل، یک زوج تصمیم میگیرند که خانه را نظافت کنند. دستمال در یک اتاق و شیشهشوی در اتاق دیگر قرار دارد. هر دو در یک لحظه بدون اینکه به دیگری چیزی بگویند تصمیم میگیرند که شیشه ها را پاک کنند. آقا به اتاق رفته و دستمال را برمیدارد و خانم هم به اتاق دیگر رفته و شیشه شوی را برمیدارد. وقتی آقا برای برداشتن شیشهشوی به اتاق دیگر میرود آن را پیدا نمیکند و وقتی خانم به اتاق دیگری میرود تا دستمال را بردارد، آن را پیدا نمیکند. بدین شکل هیچکدام نمیتوانند شیشه ها را پاک کنند.
در این مثال خانم و آقا مانند ترد هایی هستند که برای انجام وظیفه شان نیاز به منابعی دارند که در اختیار دیگری است.
اکنون بیایید ببینیم این مشکل در کد چطور رخ میدهد.
import threading
lock1 = threading.Lock()
lock2 = threading.Lock()
def thread1():
lock1.acquire()
print("Thread 1 acquired lock 1")
threading.Event().wait(1) # simulate some work
lock2.acquire() # این خط زمانی اجرا میشود که قفل آن که در خط ۱۶ گذاشته شده آزاد شود
print("Thread 1 acquired lock 2")
lock2.release()
lock1.release()
def thread2():
lock2.acquire()
print("Thread 2 acquired lock 2")
threading.Event().wait(1) # simulate some work
lock1.acquire() # این خط زمانی اجرا میشود که قفل آن که در خط ۷ گذاشته شده آزاد شود
print("Thread 2 acquired lock 1")
lock1.release()
lock2.release()
t1 = threading.Thread(target=thread1)
t2 = threading.Thread(target=thread2)
t1.start()
t2.start()
t1.join()
t2.join()
print("Finished")در تکه کد بالا:
- خط ۷: دسترسی به منبع اول قفل شده است.
- خط ۱۰: آنقدر منتظر میماند که منبع دوم آزاد شود. (هیچگاه آزاد نمیشود!)
- خط ۱۶: دسترسی به منبع دوم قفل شده است.
- خط ۱۹: آنقدر منتظر میماند که منبع اول آزاد شود. (هیچگاه آزاد نمیشود!)
برای حل این مشکل، باید قفل ها را به ترتیب بسته و باز کنید. مثلا به جای نوشتن کد ها به شکل زیر:
def thread1():
lock1.acquire()
lock2.acquire()
def thread2():
lock2.acquire()
lock1.acquire()کد ها را به شکل زیر بنویسید.
def thread1():
lock1.acquire()
lock2.acquire()
def thread2():
lock1.acquire()
lock2.acquire()سناریو سوم – مدیریت خطای اشتباه
به کد زیر توجه کنید.
import threading
lock = threading.Lock()
def divide(a, b):
lock.acquire()
try:
result = a / b
print(f"✅ result is {result}")
except ZeroDivisionError:
print("❌ Error: Can not divide by zero")
lock.release()
# استفاده در thread
t1 = threading.Thread(target=divide, args=(10, 'ali'))
t1.start()
t1.join()
t2 = threading.Thread(target=divide, args=(10, 1))
t2.start()
t2.join()
print("End of program!")در این تکه کد، برنامه نویس خطای ZeroDivisionError را به درستی مدیریت کرده اما فراموش کرده که خطای TypeError را نیز مدیریت کند. با اجرای خط ۱۵، خطای TypeError رخ میدهد و در این سناریو راهی برای آزاد کردن قفل هم در نظر گرفته نشده است.
برای حل مشکل Dead Lock در این تکه کد، میتوان آن را به شکل زیر بازنویسی کرد.
import threading
lock = threading.Lock()
def divide(a, b):
lock.acquire()
try:
result = a / b
print(f"✅ result is {result}")
except ZeroDivisionError:
print("❌ Error: Can not divide by zero")
lock.release()
except TypeError:
print("❌ Error: Invalid type")
lock.release()
# استفاده در thread
t1 = threading.Thread(target=divide, args=(10, 'ali'))
t1.start()
t1.join()
t2 = threading.Thread(target=divide, args=(10, 1))
t2.start()
t2.join()
print("End of program!")هرچند با روش بالا مشکل حل شد، اما روش حرفه ای تر برای حل این مشکل، استفاده از finally است.
import threading
lock = threading.Lock()
def divide(a, b):
lock.acquire()
try:
result = a / b
print(f"✅ result is {result}")
except ZeroDivisionError:
print("❌ Error: Can not divide by zero")
except TypeError:
print("❌ Error: Invalid type")
finally:
lock.release()
# استفاده در thread
t1 = threading.Thread(target=divide, args=(10, 'ali'))
t1.start()
t1.join()
t2 = threading.Thread(target=divide, args=(10, 1))
t2.start()
t2.join()
print("End of program!")