
مشکل: نتیجهها به ترتیبِ await میآیند، نه به ترتیبِ آمادهشدن
فرض کنید چند کار را همزمان اجرا کردهایم و میخواهیم بهمحض آمادهشدنِ هر نتیجه از آن استفاده کنیم. اگر تسکها را یکییکی await کنیم، نتیجهها فقط به همان ترتیبی که await کردهایم در دسترس قرار میگیرند — حتی اگر تسکِ دیگری زودتر تمام شده باشد. به کد زیر توجه کنید:
import asyncio
async def download_file(file_name, weight):
print(f'download {file_name} started')
await asyncio.sleep(weight)
print(f'download {file_name} finished')
return file_name
async def manage_downloads():
t1 = asyncio.create_task(download_file('test1.txt', 1))
t2 = asyncio.create_task(download_file('test2.txt', 2))
print(await t1)
print(await t2)
asyncio.run(manage_downloads())اینجا t1 زودتر (۱ ثانیه) تمام میشود، پس خروجی طبیعی بهنظر میرسد:
download test1.txt started
download test2.txt started
download test1.txt finished
test1.txt
download test2.txt finished
test2.txt
حالا وزنها را جابهجا میکنیم تا t2 زودتر تمام شود:
import asyncio
async def download_file(file_name, weight):
print(f'download {file_name} started')
await asyncio.sleep(weight)
print(f'download {file_name} finished')
return file_name
async def manage_downloads():
t1 = asyncio.create_task(download_file('test1.txt', 2))
t2 = asyncio.create_task(download_file('test2.txt', 1))
print(await t1)
print(await t2)
asyncio.run(manage_downloads())خروجی:
download test1.txt started
download test2.txt started
download test2.txt finished
download test1.txt finished
test1.txt
test2.txt
میبینید که با اینکه t2 زودتر تمام شده، باز هم اول نتیجهی t1 چاپ میشود و بعد t2؛ فقط به این دلیل که اول t1 را await کردهایم. یعنی نمیتوانیم نتیجهی زودرسِ t2 را زودتر مصرف کنیم.
راهحل: as_completed
متد asyncio.as_completed این امکان را میدهد که بین همهی تسکها، هرکدام که زودتر تمام شد، نتیجهاش را زودتر برداریم. این تابع روی مجموعهای از تسکها یک iterator میسازد که به ترتیبِ «اتمام» (نه ترتیب ورودی) نتیجهها را میدهد:
import asyncio
async def download_file(file_name, weight):
print(f'download {file_name} started')
await asyncio.sleep(weight)
print(f'download {file_name} finished')
return file_name
async def manage_downloads():
t1 = asyncio.create_task(download_file('test1.txt', 2))
t2 = asyncio.create_task(download_file('test2.txt', 1))
for task in asyncio.as_completed([t1, t2]):
print(await task) # yields whichever finishes first
asyncio.run(manage_downloads())حالا خروجی به ترتیب اتمام است؛ چون t2 زودتر تمام میشود، نتیجهاش هم زودتر مصرف میشود:
download test1.txt started
download test2.txt started
download test2.txt finished
test2.txt
download test1.txt finished
test1.txt
مثال واقعی: پرسوجو از چند سرور و پردازش تدریجی نتایج
فرض کنید همان داده را از چند سرور (mirror) درخواست میکنید و میخواهید بهمحض رسیدن هر پاسخ، آن را پردازش یا نمایش دهید — نه اینکه تا رسیدن کندترین پاسخ منتظر بمانید. این دقیقاً همان چیزی است که as_completed فراهم میکند و در نوار پیشرفت، نمایش نتایج جستوجو، یا جمعآوری زودهنگام داده بسیار کاربردی است:
import asyncio
import random
async def fetch(server_id):
delay = random.uniform(0.5, 3) # each server responds at a different time
await asyncio.sleep(delay)
return f"server {server_id} responded in {delay:.1f}s"
async def main():
tasks = [asyncio.create_task(fetch(i)) for i in range(5)]
# handle each response as soon as it arrives
for task in asyncio.as_completed(tasks):
result = await task
print("received:", result)
asyncio.run(main())نتایج بهترتیبِ سرعت پاسخدهی سرورها چاپ میشوند، نه بهترتیب شمارهی سرور. اگر فقط سریعترین پاسخ برایتان کافی است، میتوانید بعد از اولین نتیجه از حلقه خارج شوید (و در صورت نیاز بقیهی تسکها را کنسل کنید).
نکته: as_completed یک پارامتر timeout هم میگیرد؛ اگر در آن مدت همهی تسکها تمام نشوند، هنگام await کردنِ نتیجهی باقیمانده، TimeoutError پرتاب میشود.
as_completed در برابر gather
تفاوت مهم: asyncio.gather منتظر میماند تا همه تمام شوند و نتیجهها را یکجا و به ترتیب ورودی برمیگرداند. اما as_completed نتیجهها را یکییکی و به ترتیب اتمام میدهد تا بتوانید هر نتیجه را بهمحض آمادهشدن پردازش کنید. اگر نتایجِ همه را با هم و مرتب میخواهید از gather، و اگر میخواهید هر نتیجه را زودتر مصرف کنید از as_completed استفاده کنید.
جمعبندی
awaitکردنِ ترتیبیِ تسکها، نتیجهها را فقط به ترتیبawaitمیدهد، نه به ترتیب آمادهشدن.asyncio.as_completed([...])نتیجهها را به ترتیب اتمام میدهد؛ پس میتوانید هر نتیجه را بهمحض آمادهشدن مصرف کنید.- کاربرد واقعی: پردازش تدریجی نتایج، نمایش پیشرفت، یا استفاده از سریعترین پاسخ بین چند سرور.
- برای گرفتنِ همهی نتایج بهصورت مرتب و یکجا از
gather، و برای مصرف زودهنگام ازas_completedاستفاده کنید.