آموزش پایتون - بن‌بست نخ

بن‌بست (Deadlock) را می‌توان یک حالت شکست در همروندی توصیف کرد. این وضعیتی در برنامه است که در آن یک یا چند ترد (Thread) منتظر شرطی می‌مانند که هرگز رخ نمی‌دهد. در نتیجه، تردها قادر به پیشرفت نیستند و برنامه قفل یا متوقف می‌شود و باید به صورت دستی خاتمه داده شود.

وضعیت بن‌بست ممکن است به روش‌های مختلفی در برنامه همروند شما رخ دهد. بن‌بست‌ها هرگز عمداً ایجاد نمی‌شوند، بلکه در واقع یک اثر جانبی یا باگ در کد هستند.

دلایل رایج بن‌بست تردها در زیر آورده شده است −

  • تردی که تلاش می‌کند یک قفل mutex را دو بار به دست آورد.

  • تردهایی که منتظر یکدیگر می‌مانند (به عنوان مثال A منتظر B می‌ماند، B منتظر A می‌ماند).

  • زمانی که یک ترد در آزادکردن یک منبع مانند lock، semaphore، condition، event و غیره ناموفق عمل می‌کند.

  • تردهایی که قفل‌های mutex را با ترتیب‌های متفاوتی به دست می‌آورند (به عنوان مثال رعایت نکردن ترتیب قفل‌گذاری).

چگونه از بن‌بست در تردهای پایتون جلوگیری کنیم

هنگامی که چند ترد در یک برنامه چندتردی تلاش می‌کنند به یک منبع مشترک دسترسی پیدا کنند، مانند انجام عملیات خواندن/نوشتن روی یک فایل مشترک، این امر می‌تواند منجر به عدم تناقض داده‌ها شود. بنابراین، همگام‌سازی دسترسی همروند به منابع با استفاده از مکانیسم‌های قفل‌گذاری حائز اهمیت است.

ماژول threading در پایتون یک مکانیسم قفل‌گذاری با پیاده‌سازی ساده برای همگام‌سازی تردها ارائه می‌دهد. شما می‌توانید با فراخوانی کلاس Lock() یک شیء قفل جدید ایجاد کنید که قفل را در حالت قفل‌نشده مقداردهی اولیه می‌کند.

مکانیسم قفل‌گذاری با شیء Lock

یک شیء از کلاس Lock دارای دو حالت ممکن است − قفل‌شده یا قفل‌نشده، که در اولین زمان ایجاد در حالت قفل‌نشده قرار دارد. یک قفل متعلق به هیچ ترد خاصی نیست.

کلاس Lock متدهای acquire() و release() را تعریف می‌کند.

متد acquire()

متد acquire() از کلاس Lock حالت قفل را از قفل‌نشده به قفل‌شده تغییر می‌دهد. این متد بلافاصله بازمی‌گردد، مگر اینکه آرگومان اختیاری blocking برابر با True تنظیم شده باشد که در این صورت تا زمان به دست آمدن قفل منتظر می‌ماند.

در اینجا ساختار (Syntax) این متد آورده شده است −

Lock.acquire(blocking, timeout)

که در آن،

  • blocking − اگر برابر با False تنظیم شود، به این معنی است که مسدودسازی انجام نشود. اگر فراخوانی با blocking تنظیم‌شده روی True منجر به مسدودسازی شود، بلافاصله False را برمی‌گرداند؛ در غیر این صورت، قفل را روی قفل‌شده تنظیم کرده و True را برمی‌گرداند.

  • timeout − یک بازه زمانی مهلت (timeout) برای به دست آوردن قفل تعیین می‌کند.

مقدار بازگشتی این متد در صورت موفقیت‌آمیز بودن دریافت قفل True و در غیر این صورت False است.

متد release()

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

در ادامه ساختار (Syntax) متد release() آمده است −

Lock.release()

متد release() فقط باید در حالت قفل‌شده فراخوانی شود. اگر تلاشی برای آزادکردن یک قفلِ قفل‌نشده صورت گیرد، یک RuntimeError رخ خواهد داد.

هنگامی که قفل در حالت قفل‌شده است، آن را به قفل‌نشده بازنشانی کرده و بازمی‌گردد. اگر هر ترد دیگری منتظر قفل‌نشده شدن قفل مسدود شده باشد، دقیقاً به یکی از آن‌ها اجازه پیشرفت داده می‌شود. این متد هیچ مقدار بازگشتی ندارد.

مثال

در برنامه زیر، دو ترد تلاش می‌کنند متد synchronized() را فراخوانی کنند. یکی از آن‌ها قفل را به دست می‌آورد و دسترسی پیدا می‌کند در حالی که دیگری منتظر می‌ماند. زمانی که متد run() برای ترد اول کامل شد، قفل آزاد می‌شود و متد همگام‌سازی‌شده برای ترد دوم در دسترس قرار می‌گیرد.

هنگامی که هر دو ترد بپیوندند (join)، برنامه به پایان می‌رسد.

from threading import Thread, Lock
import time

lock=Lock()
threads=[]

class myThread(Thread):
   def __init__(self,name):
      Thread.__init__(self)
      self.name=name
   def run(self):
      lock.acquire()
      synchronized(self.name)
      lock.release()

def synchronized(threadName):
   print ("{} has acquired lock and is running synchronized method".format(threadName))
   counter=5
   while counter:
      print ('**', end='')
      time.sleep(2)
      counter=counter-1
   print('\nlock released for', threadName)

t1=myThread('Thread1')
t2=myThread('Thread2')

t1.start()
threads.append(t1)

t2.start()
threads.append(t2)

for t in threads:
   t.join()
print ("end of main thread")

این برنامه خروجی زیر را تولید خواهد کرد −

Thread1 has acquired lock and is running synchronized method
**********
lock released for Thread1
Thread2 has acquired lock and is running synchronized method
**********
lock released for Thread2
end of main thread

شیء Semaphore برای همگام‌سازی

علاوه بر قفل‌ها، ماژول threading پایتون از semaphoreها نیز پشتیبانی می‌کند که تکنیک همگام‌سازی دیگری را ارائه می‌دهند. این یکی از قدیمی‌ترین تکنیک‌های همگام‌سازی است که توسط دانشمند مشهور علوم کامپیوتر، ادسخر دبلیو. دیسترا اختراع شده است.

مفهوم پایه سامافور (Semaphore) استفاده از یک شمارنده داخلی است که با هر فراخوانی acquire() کاهش یافته و با هر فراخوانی release() افزایش می‌یابد. این شمارنده هرگز نمی‌تواند به زیر صفر برسد؛ وقتی acquire() متوجه شود که شمارنده صفر است، مسدود می‌شود و منتظر می‌ماند تا ترد دیگری release() را فراخوانی کند.

کلاس Semaphore در ماژول threading متدهای acquire() و release() را تعریف می‌کند.

متد acquire()

اگر شمارنده داخلی در هنگام ورود بزرگتر از صفر باشد، آن را یک واحد کاهش داده و بلافاصله True را برمی‌گرداند.

اگر شمارنده داخلی در هنگام ورود صفر باشد، تا زمانی که با فراخوانی release() بیدار شود مسدود می‌شود. پس از بیدار شدن (و بیشتر بودن شمارنده از 0)، شمارنده را 1 واحد کاهش داده و True را برمی‌گرداند. دقیقا یک ترد با هر فراخوانی release() بیدار می‌شود. ترتیبی که تردها بیدار می‌شوند اختیاری است.

اگر پارامتر blocking برابر با False تنظیم شده باشد، مسدودسازی انجام ندهید. اگر یک فراخوانی بدون آرگومان مسدود شود، بلافاصله False را برمی‌گرداند؛ در غیر این صورت، همان کاری را انجام می‌دهد که در صورت فراخوانی بدون آرگومان انجام می‌داد، و True را برمی‌گرداند.

متد release()

یک سامافور را آزاد کرده و شمارنده داخلی را 1 واحد افزایش می‌دهد. زمانی که در هنگام ورود صفر بوده و تردهای دیگری منتظر بزرگتر شدن مجدد آن از صفر هستند، n عدد از آن تردها را بیدار می‌کند.

مثال

این مثال نحوه استفاده از یک شیء Semaphore در پایتون را برای کنترل دسترسی به یک منبع مشترک بین چندین ترد، به منظور جلوگیری از بن‌بست در برنامه چندتردی پایتون نشان می‌دهد.

from threading import *
import time

# creating thread instance where count = 3
lock = Semaphore(4)

# creating instance
def synchronized(name):
   
   # calling acquire method
   lock.acquire()

   for n in range(3):
      print('Hello! ', end = '')
      time.sleep(1)
      print( name)

      # calling release method
      lock.release()

# creating multiple thread
thread_1 = Thread(target = synchronized , args = ('Thread 1',))
thread_2 = Thread(target = synchronized , args = ('Thread 2',))
thread_3 = Thread(target = synchronized , args = ('Thread 3',))

# calling the threads
thread_1.start()
thread_2.start()
thread_3.start()

این برنامه خروجی زیر را تولید خواهد کرد −

Hello! Hello! Hello! Thread 1
Hello! Thread 2
Thread 3
Hello! Hello! Thread 1
Hello! Thread 3
Thread 2
Hello! Hello! Thread 1
Thread 3
Thread 2

پیشرفت مطالعه: وضعیت خوانده‌شدن این درس فقط در همین مرورگر ذخیره می‌شود؛ بدون نیاز به حساب کاربری.