8.4. Gather¶
أظهر الفصل السابق كيف يجدول asyncio.create_task() دالة تعاونية ولا ينتظرها. أما العملية المرافقة -- تشغيل عدة كائنات قابلة للانتظار بشكل متزامن وانتظارها جميعاً -- فهي asyncio.gather().
8.4.1. الشكل الأساسي¶
import asyncio
async def fetch(name, delay_ms):
await asyncio.sleep_ms(delay_ms)
return name
async def main():
results = await asyncio.gather(
fetch("a", 100),
fetch("b", 200),
fetch("c", 50),
)
print(results)
asyncio.run(main())
المُخرَج:
['a', 'b', 'c']
هناك أمران يجب ملاحظتهما. أولاً، قائمة النتائج بترتيب وسائط gather، وليس بالترتيب الذي انتهت فيه الدوال التعاونية -- فقد أعادت "c" أولاً لكنها لا تزال المُدخَل الثالث. ثانياً، استغرق الاستدعاء 200 ms إجمالاً، لا 350 ms: فقد عملت فترات النوم الثلاث بشكل متزامن، وتُرجع gather() حالما تكتمل أبطؤها.
ينبع الأمران من المصدر ذاته. فـ gather() تغلّف كل وسيطة ليست مهمةً بالفعل (الدوال التعاونية في المثال)، وتجدولها على الحلقة، وتعلّق الدالة التعاونية المستدعية حتى ينتهي كل منها، ثم تُرجع نتائجها بالترتيب الأصلي.
8.4.2. متى تلجأ إليها¶
في أي مكان يكون فيه لدى التطبيق عدد N من العمليات القابلة للانتظار ويريد نتائجها جميعاً قبل المتابعة. أمثلة نموذجية:
إصدار عدة طلبات شبكة بالتوازي وانتظار جميع الاستجابات.
القراءة من عدة مستشعرات بشكل متزامن قبل معالجة النتيجة المجمّعة.
ضمّ عدة مهام مساعدة قصيرة العمر في نهاية مرحلة من مراحل التطبيق.
وهي ليست الأداة المناسبة للمهام الخلفية طويلة التشغيل التي يبدؤها التطبيق ويتركها تعمل طوال عمر البرنامج -- فتلك لا تزال من عمل create_task(). أما gather() فهي لنمط التوزّع/التجمّع (fan-out / fan-in): تقسيم العمل، وتنفيذه بشكل متزامن، ثم إعادة الضمّ.
8.4.3. الاستثناءات في المجموعة¶
إذا أثار أيٌّ من الكائنات القابلة للانتظار المجمّعة استثناءً، فالسلوك الافتراضي هو إعادة إثارة الاستثناء خارج استدعاء gather. ويُلغى في الخلفية الأشقاء الذين لم ينتهوا بعد.
هذا عادةً ما يريده التطبيق -- فشلت إحدى المهام المتوازية الـ N، فبالتالي فشلت العملية المجمّعة، فيتوقف إنفاق الوقت على الباقي. وأحياناً يريد التطبيق العكس: ترك كل كائن قابل للانتظار ينتهي (أو يفشل) بشكل مستقل، ثم فحص النتائج بعد ذلك. مرّر return_exceptions=True لتحقيق ذلك:
results = await asyncio.gather(
fetch_or_fail("ok"),
fetch_or_fail("bad"),
return_exceptions=True,
)
# results == ["ok-value", OSError(...)]
أصبح كل مُدخَل في القائمة المُرجَعة الآن إما قيمة إرجاع عادية أو الاستثناء الذي أثاره الكائن القابل للانتظار المقابل. ويتحقق المستدعي من isinstance(r, Exception) للتمييز بينها.
8.4.4. الإلغاء¶
إلغاء الـ gather نفسه -- بإلغاء أي مهمة كانت تنتظره -- يلغي كل كائن قابل للانتظار لا يزال يعمل بداخله. تغطي صفحة المهل الزمنية والإلغاء كيفية انتشار الإلغاء عبر سلسلة الاستدعاء بالتفصيل.