مقدمه
صور کنید که یک جعبه شکلات دارید و دو تا از دوستهایتان میخواهند همزمان از این جعبه شکلات بردارند. اگر هر دو به طور همزمان دستشان را داخل جعبه کنند، ممکن است دستهایشان به هم بخورد کند و شکلاتها بریزند. برای جلوگیری از این اتفاق، نیاز به نوعی هماهنگی داریم.
وقتی میگوییم یک کد “thread safe” است، منظور این است که اگر چندین نفر (در اینجا چندین thread یا رشته) بخواهند همزمان از یک جعبه مشترک (مثل یک متغیر یا منبع مشترک) استفاده کنند، هیچ تداخلی پیش نمیآید و همه چیز به خوبی کار میکند. حالا بیایید این مفهوم را با یک مثال ساده در پایتون نشان دهیم.
مثال – برداشتن و اضافه کردن به یک شمارنده
فرض کنید که یک شمارنده داریم و دو نفر میخواهند همزمان این شمارنده را تغییر دهند. یکی میخواهد شمارنده را افزایش دهد و دیگری میخواهد آن را کاهش دهد.
کدی که thread safe نیست:
from threading import Thread
# تعریف شمارنده مشترک
counter = 0
def add():
global counter
for _ in range(100000):
counter += 1
def subtract():
global counter
for _ in range(100000):
counter -= 1
# ایجاد و شروع رشتهها
thread1 = Thread(target=add)
thread2 = Thread(target=subtract)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
# چاپ مقدار نهایی شمارنده
print(counter)در این کد، چون هر دو thread همزمان به شمارنده دسترسی دارند، ممکن است به نتیجه صحیح نرسیم. به این میگویند کدی که thread safe نیست. حالا بیایید این کد را به گونهای تغییر دهیم که thread safe شود.
ساختن کد thread safe با استفاده از قفل (Lock)
وقتی میخواهیم مطمئن شویم که فقط یک نفر در هر زمان به جعبه شکلات (شمارنده) دسترسی دارد، میتوانیم از چیزی به نام “قفل” (Lock) استفاده کنیم. این قفل مثل درب قفل شده جعبه شکلات است که فقط یک نفر میتواند آن را باز کند و بعد از برداشتن شکلات، درب را میبندد تا نفر بعدی بتواند آن را باز کند.
کدی که thread safe است:
from threading import Thread, Lock
# تعریف شمارنده مشترک و قفل
counter = 0
lock = Lock()
def add():
global counter
for _ in range(100000):
with lock: # گرفتن قفل
counter += 1 # تغییر شمارنده
# آزاد کردن قفل به طور خودکار
def subtract():
global counter
for _ in range(100000):
with lock: # گرفتن قفل
counter -= 1 # تغییر شمارنده
# آزاد کردن قفل به طور خودکار
# ایجاد و شروع رشتهها
thread1 = Thread(target=add)
thread2 = Thread(target=subtract)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
# چاپ مقدار نهایی شمارنده
print(counter)قفل (Lock): یک قفل مثل درب جعبه شکلات است. وقتی یک نفر (یک thread) میخواهد از جعبه شکلات بردارد، درب را قفل میکند و بعد از برداشتن شکلات، درب را باز میکند تا نفر بعدی بتواند از جعبه استفاده کند.
کد thread safe: در اینجا از with استفاده کردیم که مطمئن میشود فقط یک نفر در هر زمان به شمارنده دسترسی دارد و بعد از تغییر شمارنده، قفل را باز میکند تا نفر بعدی بتواند از آن استفاده کند.
این تغییرات باعث میشود که کد ما thread safe باشد، یعنی بدون هیچ مشکلی حتی با چندین نفر (threads) که همزمان کار میکنند، کار میکند.
نکته مهم در مورد مفهوم thread safe و Race Condition
آیا ممکن است کدی مشکل race condition نداشته باشد اما thread safe نباشد؟
کدی که مشکل race condition دارد، thread safe نیست اما کدی که مشکل race condition ندارد لزوما thread safe نیست! اینکه تصور کنید با حل کردن مشکل Race Condition کدتان thread safe میشود، تصور اشتباهی است.
بیایید یک بار دیگر به تعریف race condition و thread safe توجه کنیم:
Race Condition: زمانی اتفاق میافتد که خروجی یک برنامه به ترتیب و زمانبندی دسترسی به متغیرهای مشترک وابسته باشد. این امر منجر به رفتار غیرقابل پیشبینی میشود.
Thread Safe: به این معنی است که یک برنامه یا بخشی از کد، بدون مشکل میتواند در شرایط همزمانی توسط چندین thread اجرا شود، بدون اینکه باعث رفتار غیرمنتظره یا نادرست شود.
طبق این تعاریف، این دو مفهوم با یکدیگر ارتباط دارند اما ارتباطشان اینگونه نیست که بتوان نتیجه گرفت هر کدی که Race Condition ندارد پس thread safe است.
آیا ممکن است کدی مشکل race condition نداشته باشد اما thread safe نباشد؟
در برخی موارد خاص، ممکن است کدی که مشکل race condition ندارد همچنان thread safe نباشد. این موارد معمولاً پیچیدهتر و کمتر رایج هستند. در ادامه برخی از این موارد را بررسی میکنیم.
کدهایی که به منابع خارجی وابسته هستند: حتی اگر کد مشکل race condition نداشته باشد، ممکن است به درستی در شرایط همزمانی کار نکند. مثلاً دسترسی به یک فایل یا پایگاه داده خارجی که خود thread safe نیست.
منابع مشترک غیرقابل پیشبینی: منابعی مثل متغیرهای محیطی یا تنظیمات سیستمی که ممکن است به طور غیرمنتظرهای تغییر کنند.
عملیات غیراتمی: عملیاتهایی که به طور طبیعی در چندین مرحله انجام میشوند و نمیتوانند به طور کامل در یک مرحله به پایان برسند.
مثال – لاگ نویسی
فرض کنید یک برنامه دارید که یک فایل لاگ را مینویسد. این کد مشکل race condition ندارد اما ممکن است thread safe نباشد زیرا حتی اگر دسترسی همزمان به فایل را با قفل کنترل کرده باشید، اگر فایل سیستم به درستی همگامسازی نشده باشد یا عملیات نوشتن در فایل غیراتمی باشد، کد ممکن است همچنان رفتار نادرستی نشان دهد.
from threading import Thread, Lock
log_lock = Lock()
def write_log(message):
with log_lock:
with open("log.txt", "a") as log_file:
log_file.write(message + "\n")
def thread1_task():
for _ in range(100):
write_log("Thread 1 is running")
def thread2_task():
for _ in range(100):
write_log("Thread 2 is running")
thread1 = Thread(target=thread1_task)
thread2 = Thread(target=thread2_task)
thread1.start()
thread2.start()
thread1.join()
thread2.join()در این مثال، اگرچه از قفل برای دسترسی به فایل استفاده شده، اما اگر سیستم فایل به درستی همگامسازی نشده باشد یا مشکل دیگری وجود داشته باشد، ممکن است لاگهای نادرستی تولید شود. به عبارت دیگر، در سطح کد ما race condition نداریم، اما همچنان ممکن است مشکلاتی در همزمانی رخ دهد که کد را thread safe نکند.
موارد خاصی که ممکن است سیستم فایل به درستی همگامسازی نشده باشد و در صورت رخ دادن برنامه را از حالت thread safe بودن خارج میکنند به شرح زیر هستند:
سیستمهای فایل شبکهای (Network File Systems): در سیستمهای فایل شبکهای مثل NFS (Network File System)، ممکن است تاخیرهای شبکه یا مشکلات همگامسازی باعث شوند که دادهها به طور ناهمگام (asynchronously) نوشته شوند. این ممکن است باعث شود که تغییرات فایل در یک کامپیوتر بلافاصله در کامپیوتر دیگر دیده نشود.
کش کردن (Caching): برخی سیستمهای فایل از کش کردن برای افزایش کارایی استفاده میکنند. این میتواند باعث شود که دادهها ابتدا در حافظه کش ذخیره شده و سپس به دیسک نوشته شوند. اگر کش به درستی همگامسازی نشود (مثلاً در صورت قطع ناگهانی برق)، ممکن است دادهها از دست بروند.
سیستمهای فایل مجازی (Virtual File Systems): برخی سیستمهای فایل مجازی که روی سختافزارهای خاصی اجرا میشوند، ممکن است به درستی همگامسازی نشده باشند. این مشکلات میتوانند به دلیل پیادهسازی نادرست یا محدودیتهای سختافزاری باشند.
بازیابی از خرابی (Recovery from Crashes): اگر سیستم در حال نوشتن دادهها به فایل باشد و ناگهان سیستم کرش کند، ممکن است دادهها به طور کامل نوشته نشوند. برخی سیستمهای فایل از تکنیکهایی مثل journaling استفاده میکنند تا مطمئن شوند که دادهها به درستی بازیابی میشوند، اما در برخی موارد همچنان ممکن است مشکلاتی پیش بیاید.
نتیجهگیری
به طور کلی، حل مشکل race condition یک گام مهم برای رسیدن به thread safety است، اما تضمین کامل thread safety نیاز به در نظر گرفتن تمامی جنبههای همزمانی و منابع مشترک دارد.