یک Consumer نوشتهاید که باید منتظر بماند تا در صف چیزی باشد، اما بهجای انتظار، مدام CPU را میسوزاند یا بدتر، آیتمی را که هنوز نرسیده pop میکند و برنامه با IndexError میترکد. راهحلِ نصفهونیمه معمولاً یک حلقهٔ while با asyncio.sleep است که هم زشت است و هم شرط را بهدرستی هماهنگ نمیکند. اینجا دقیقاً جایی است که کلاس Condition در asyncio وارد میشود و مشکل را تمیز حل میکند.
چرا Event کافی نیست؟
در درس قبل asyncio.Event را دیدیم که فقط یک سیگنال روشن/خاموش میفرستد و هیچ اطلاعی از وضعیت دادههای مشترک ندارد. اما خیلی وقتها سؤال این نیست که «آیا اتفاقی افتاد؟» بلکه این است که «آیا این شرط خاص روی دادهٔ مشترک برقرار شد؟» مثلاً «صف خالی نباشد». کلاس Condition یک Lock را با مکانیزم wait/notify ترکیب میکند تا هم دسترسی همزمان را ایمن کند و هم انتظار مبتنی بر شرط را ممکن سازد.
ساخت و ساختار کلاس Condition
با asyncio.Condition() یک شیء میسازید که درونش یک asyncio.Lock نهفته است. هر دستکاری روی دادهٔ مشترک باید داخل async with condition انجام شود؛ این بلوک همان Lock را میگیرد و آزاد میکند. متدهای کلیدی:
await condition.wait(): باید داخلasync withباشد؛ Lock را موقتاً آزاد میکند، منتظرnotifyمیماند و بعد دوباره Lock را میگیرد.await condition.wait_for(predicate): تا وقتی تابع شرطTrueنشده، خودکارwaitرا تکرار میکند.condition.notify(n=1): یک (یا n) Task منتظر را بیدار میکند.condition.notify_all(): همهٔ منتظرها را بیدار میکند.
مثال واقعی: Producer و Consumer با یک شرط مشخص
Producer آیتم تولید میکند و Consumer فقط وقتی باید بیدار شود که لیست مشترک حداقل یک عضو داشته باشد. این شرط را دقیقاً به wait_for میسپاریم:
import asyncio
import random
async def producer(condition, items):
for i in range(5):
await asyncio.sleep(random.uniform(0.2, 0.6))
async with condition: # take the lock before touching shared data
items.append(f'item-{i}')
print(f'producer -> item-{i}')
condition.notify() # wake a waiting consumer
async def consumer(condition, items):
while True:
async with condition:
# sleep until there is really something to take
await condition.wait_for(lambda: len(items) > 0)
item = items.pop(0)
print(f'consumer <- {item}')
if item == 'item-4':
break
async def main():
condition = asyncio.Condition()
items = []
await asyncio.gather(
producer(condition, items),
consumer(condition, items),
)
asyncio.run(main())
حالا Consumer دیگر هرگز روی لیست خالی pop نمیکند، چون wait_for تضمین میکند تا وقتی شرط برقرار نشده، بیدار نمیشود. اگر هم به اشتباه از notify بیمورد بیدار شود، شرط دوباره چک میشود و بیسروصدا برمیگردد به خواب.
دام رایج: notify بیرون از بلوک
رایجترین اشتباه این است که دادهٔ مشترک را تغییر دهید ولی notify را بیرون از همان async with صدا بزنید. دلیلش که مهم است: notify فقط زمانی معنا دارد که Lock در دست شماست؛ اگر بیرون از بلوک صدایش بزنید، هماهنگیِ بیدارسازی از دست میرود و ممکن است Task منتظر اصلاً از تغییر باخبر نشود یا با شرطِ ناسازگار بیدار شود. پس همیشه: تغییر داده و notify، هر دو داخل یک بلوک.
کِی از Condition استفاده کنیم؟
اگر فقط یک سیگنال ساده لازم دارید، Event سبکتر است. اگر میخواهید تعداد Taskهای همزمان را محدود کنید، سراغ Semaphore بروید. اما وقتی چند Task باید بر اساس وضعیت یک دادهٔ مشترک با هم هماهنگ شوند، Condition انتخاب درست است.
جمعبندی
Conditionیک Lock را باwait/notifyترکیب میکند تا انتظارِ مبتنی بر شرط ممکن شود.- از
wait_for(predicate)استفاده کنید تا Task فقط با برقراری شرط واقعی بیدار شود و از bug مثل pop روی لیست خالی جلوگیری شود. - تغییر دادهٔ مشترک و
notifyرا همیشه داخل یک بلوکasync withنگه دارید. - برای سیگنال ساده
Eventو برای محدودکردن همزمانیSemaphoreمناسبترند؛Conditionبرای هماهنگی بر پایهٔ شرط است.