
کنسلکردن یک coroutine
گاهی یک تسک بیش از حد طول میکشد و میخواهید آن را متوقف کنید؛ مثلاً دانلودی که خیلی کند شده یا درخواستی که پاسخ نمیدهد. در این حالت میتوانید تسک را با متد cancel کنسل کنید. در این درس سه راه برای کنترل زمان و کنسلکردن یک تسک میبینیم و یاد میگیریم چطور یک تسک را از کنسلشدن محافظت کنیم.
روش اول: کنسل دستی با cancel()
در این روش یک تسک میسازیم و در یک حلقه وضعیتش را زیر نظر میگیریم؛ اگر تا ۵ ثانیه تمام نشد، آن را cancel میکنیم. کنسلکردن یک تسک باعث میشود در نقطهی await جاری، خطای CancelledError پرتاب شود که آن را میگیریم:
import asyncio
async def download_file(file_name):
print(f'download {file_name} started')
await asyncio.sleep(6) # simulate a 6-second download
print(f'download {file_name} finished')
async def manage_downloads():
t1 = asyncio.create_task(download_file('test1.txt'))
seconds = 0
while not t1.done():
print('download still running')
await asyncio.sleep(1)
seconds += 1
if seconds == 5:
t1.cancel() # cancel after 5 seconds
try:
await t1
except asyncio.CancelledError:
print('download cancelled!')
asyncio.run(manage_downloads())چون دانلود ۶ ثانیه طول میکشد اما ما بعد از ۵ ثانیه کنسلش میکنیم، پیام «finished» هرگز چاپ نمیشود و بهجای آن «download cancelled!» را میبینیم. توجه کنید که پس از cancel حتماً باید تسک را await کنیم تا CancelledError واقعاً بروز کند و بتوانیم مدیریتش کنیم.
روش دوم و بهتر: wait_for با timeout
نوشتن دستیِ آن حلقه شلوغ و مستعد خطاست. راه تمیزتر، استفاده از asyncio.wait_for است که خودش یک timeout میگیرد و اگر تسک در آن مدت تمام نشد، آن را کنسل میکند. یک نکتهی مهم: wait_for هنگام timeout، خطای asyncio.TimeoutError پرتاب میکند (نه CancelledError)؛ پس باید همان را بگیریم:
import asyncio
async def download_file(file_name):
print(f'download {file_name} started')
await asyncio.sleep(6)
print(f'download {file_name} finished')
async def manage_downloads():
t1 = asyncio.create_task(download_file('test.txt'))
try:
await asyncio.wait_for(t1, timeout=5)
except asyncio.TimeoutError: # wait_for raises TimeoutError on timeout
print('download cancelled (timed out)!')
asyncio.run(manage_downloads())وقتی timeout رخ میدهد، wait_for در پشتصحنه تسک را کنسل میکند و سپس TimeoutError میدهد. این کد کوتاهتر و خواناتر از روش دستی است.
مثال – محدودکردن زمان یک درخواست شبکه
رایجترین کاربرد واقعیِ این الگو، تحمیل یک «سقف زمانی» به درخواستهای شبکه است تا برنامه بهخاطر یک سرور کند برای همیشه معطل نماند. مثلاً «اگر پاسخ API در ۳ ثانیه نرسید، بیخیالش شو و یک مقدار پیشفرض برگردان»:
import asyncio
async def call_api():
await asyncio.sleep(10) # a slow/hanging API (in real code: a network request)
return {"status": "ok"}
async def get_data_with_timeout():
try:
return await asyncio.wait_for(call_api(), timeout=3)
except asyncio.TimeoutError:
print("API too slow, using a default value")
return {"status": "unknown"}
async def main():
data = await get_data_with_timeout()
print("result:", data)
asyncio.run(main())نکته: در پایتون ۳.۱۱ به بعد میتوانید از context manager تمیزترِ async with asyncio.timeout(3): هم برای همین کار استفاده کنید.
import asyncio
async def call_api():
await asyncio.sleep(10) # a slow/hanging API (in real code: a network request)
return {"status": "ok"}
async def get_data_with_timeout():
try:
async with asyncio.timeout(3):
return await call_api()
except TimeoutError:
print("API too slow, using a default value")
return {"status": "unknown"}
async def main():
data = await get_data_with_timeout()
print("result:", data)
asyncio.run(main())محافظت از یک تسک در برابر کنسلشدن با shield
گاهی میخواهیم زمانسنجی کنیم اما نمیخواهیم تسک واقعاً کنسل شود (مثلاً کاری که نصفهکارهماندنش خطرناک است). متد asyncio.shield یک coroutine را از کنسلشدن محافظت میکند؛ اما اگر timeout رخ دهد باز هم TimeoutError میگیریم و کنترل از wait_for خارج میشود — با این تفاوت که خودِ تسکِ محافظتشده در پسزمینه به کارش ادامه میدهد و در صورت نیاز باید دوباره آن را await کنیم:
import asyncio
async def download_file(file_name):
print(f'download {file_name} started')
await asyncio.sleep(6)
print(f'download {file_name} finished')
async def manage_downloads():
t1 = asyncio.create_task(download_file('test.txt'))
try:
await asyncio.wait_for(asyncio.shield(t1), timeout=5)
except asyncio.TimeoutError:
print('downloading takes too long, but we are still working on it')
await t1 # the shielded task kept running; wait for it to finish
asyncio.run(manage_downloads())تفاوت با روش قبل: اینجا با اینکه ۵ ثانیه گذشت و TimeoutError گرفتیم، دانلود کنسل نشد و در پسزمینه ادامه یافت؛ بعد با await t1 منتظر پایانش ماندیم. کاربرد واقعی shield برای عملیاتی است که نباید نصفه رها شوند — مثل ثبت نهایی یک تراکنش یا ذخیرهی یک فایل — که حتی با رسیدن timeout هم باید تا انتها اجرا شوند.
جمعبندی
- یک تسک را با
task.cancel()کنسل میکنیم؛ سپس باawaitکردن آن،asyncio.CancelledErrorرا میگیریم و مدیریت میکنیم. - راه تمیزتر برای «سقف زمانی»،
asyncio.wait_for(task, timeout=...)است که هنگام timeout تسک را کنسل وasyncio.TimeoutErrorپرتاب میکند. - با
asyncio.shield(task)میتوان تسک را از کنسلشدن محافظت کرد؛ در این حالت timeout باز همTimeoutErrorمیدهد اما تسک ادامه مییابد. - کاربرد اصلی این ابزارها، تحمیل سقف زمانی به درخواستهای شبکه است تا برنامه بهخاطر یک عملیات کند معلق نماند.