تصور کنید گه پردازش همزمان میخواهند از یک سرور دانلود فایل بگیرند، اما سرور فقط تحمل دو اتصال همزمان را دارد؛ اگر همه با هم هجوم ببرند، یا خطا میگیرید یا سرعت بهشدت افت میکند. اینجا دقیقاً جایی است که Semaphore در multiprocessing به کار میآید: یک شمارندهی مشترک که میگوید «حداکثر N پردازش با هم اجازهی ورود دارند، بقیه صف بکشند». اگر با نسخهی threading کار کرده باشید، مفهوم دقیقاً همان است؛ فقط اینبار بهجای ترد، پردازش داریم.
بیایید اول ببینیم مشکل بدون کنترل چه شکلی است. فرض کنید پنج پردازش میسازیم که همگی همزمان وارد یک بخش حساس میشوند. خروجی درهم و برهم میشود و منبع محدود ما زیر فشار له میشود. راهحل، محدود کردن تعداد ورودهای همزمان است.
ساخت یک Semaphore در multiprocessing
ساختنش یک خط است؛ عددی که میدهید همان سقف دسترسی همزمان است:
sem = multiprocessing.Semaphore(2) # at most 2 processes inside at once
یک نکتهی کلیدی که تازهکارها زیاد فراموشش میکنند: پردازشها حافظه را مثل تردها به اشتراک نمیگذارند. پس نمیتوانید sem را یک متغیر سراسری بگذارید و انتظار داشته باشید همه ببینندش. باید آن را صریحاً بهعنوان آرگومان به تابع هدفِ هر پردازش پاس بدهید.
مثال واقعی: سقف دو دانلود همزمان
پنج کارگر میسازیم، اما فقط دو تا در هر لحظه اجازهی ورود به بخش دانلود را دارند. با عبارت with sem هم گرفتن و هم آزاد کردن مجوز خودکار انجام میشود:
import multiprocessing
import time
def download(sem, worker_id):
with sem: # acquire a slot; auto-release on exit
print(f'worker-{worker_id}: downloading...')
time.sleep(1) # simulate network work
print(f'worker-{worker_id}: done')
if __name__ == '__main__': # required guard on Windows/macOS spawn
sem = multiprocessing.Semaphore(2)
workers = []
for i in range(5):
p = multiprocessing.Process(target=download, args=(sem, i))
workers.append(p)
p.start()
for p in workers:
p.join() # wait for all to finish
اگر اجرا کنید میبینید که پیامهای «downloading» دوتادوتا ظاهر میشوند، نه هر پنجتا با هم. چرا؟ چون سومین پردازش پشت در میماند تا یکی از آن دو جای اشغالشده آزاد شود. همین رفتار ساده، دقیقاً چیزی است که سرور محدود ما لازم داشت.
قرار دادن کل کد داخل if __name__ == '__main__': هم اختیاری نیست؛ روی ویندوز و macOS که پردازشها با روش spawn ساخته میشوند، بدون این محافظ برنامه خودش را بینهایت بار اجرا میکند.
BoundedSemaphore و یک تلهی رایج
یک اشتباه پرتکرار این است که جایی release() را دستی بیشتر از acquire() صدا بزنید؛ آنوقت شمارنده بیسروصدا از مقدار اولیه بالاتر میرود و ناگهان بهجای دو، سه پردازش با هم وارد میشوند. دلیلش این است که Semaphore معمولی سقف را چک نمیکند. راهحل، استفاده از multiprocessing.BoundedSemaphore است که اگر release بیش از حد مجاز فراخوانی شود، بلافاصله استثنا پرتاب میکند و اشتباه را همانجا لو میدهد. به همین دلیل در کد جدی بهتر است همیشه نسخهی Bounded را ترجیح بدهید (مثل نسخهی threading آن).
کِی از Semaphore استفاده کنیم؟
اگر فقط میخواهید یک منبع را کاملاً انحصاری کنید (یک پردازش در یک زمان)، Lock کافی و سبکتر است. سراغ Semaphore بروید وقتی میخواهید «چند تا، اما نه بینهایت» را اجازه بدهید: سقف اتصال به دیتابیس، تعداد دانلود همزمان، یا تعداد کارگرهایی که به یک API نرخمحدود میزنند. مزیتش سادگی است؛ هزینهاش این است که خودش هیچ عدالتی تضمین نمیکند و ترتیب دقیق ورود پردازشها قطعی نیست.
جمعبندی
Semaphore(N)تعداد پردازشهای همزمان در بخش حساس را به N محدود میکند.- چون حافظه مشترک نیست، شیء را حتماً بهعنوان آرگومان به پردازشها پاس بدهید.
- کل کد اجراکننده باید زیر
if __name__ == '__main__':باشد. - برای امنیت بیشتر و گرفتن باگهای
releaseاضافی، ازBoundedSemaphoreاستفاده کنید.