بررسی مشکل Dead Lock در پایتون

Please login to bookmark Close

مقدمه

اگر دیدید یک برنامه مولتی ترد اجرا نمی‌شود و انگار مرده است، برنامه دچار مشکل 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!")
Please login to bookmark Close
نظرات

دیدگاهتان را بنویسید

فهرست مطالب

سرفصل دوره

تمرین

این قسمت تمرین ندارد!

پاسخ تمرین ها

هنوز برای تمرین‌های این قسمت پاسخی ثبت نشده است!

اشتراک گذاری

چرا بهتره از فیلترشکن استفاده کنید؟

من همه ویدئو ها و پادکست های کُدباز رو توی یوتیوب و ساندکلود و پلتفرم هایی آپلود می‌کنم که اغلب فیلتر هستند.

اغلب آموزش‌ها ویدئو و پادکست دارند. پس اگر می‌خواهید از محتوای سایت بیشترین استفاده رو ببرید نیاز به فیلتر شکن دارید.

توجه داشته باشید که برای خرید از فروشگاه بهتره فیلتر شکن رو خاموش کنید.

تنظیمات

انتخاب زبان
تغییر تم