افهم التزامن التعاوني في asyncio: الكوروتينات الكسولة، ونقاط التعليق، وgather، وTaskGroup، وفخاخ الاستدعاءات الحاجبة.
افتح هذا الدرس في Kodokonتُطبّق asyncio التزامنًا تعاونيًا على خيط واحد: إذ تنظّم حلقة أحداث الكوروتينات (coroutines) التي تتخلّى طوعًا عن التحكم عند كل await. ونقطة جوهرية كثيرًا ما يُساء فهمها: استدعاء دالة async def لا ينفّذ شيئًا. فهو يبني كائن كوروتين خاملًا لا يتقدّم جسمه إلا عندما يُنتظَر بـ await أو يُجدوَل كمهمة على الحلقة.
import asyncio
async def greet() -> str:
return "hello"
coro = greet()
print(type(coro).__name__)
async def main() -> None:
print(await coro)
asyncio.run(main())تشير await إلى نقطة تعليق: يتجمّد الكوروتين عندها، وتستعيد الحلقة التحكم فتُقدّم مهمة أخرى جاهزة. وبين كل await والتالية، يعمل كودك دون انقطاع - فلا حاجة إلى أقفال لمعظم البنى. ولإطلاق عدة كوروتينات على التوازي (بمعنى الإدخال/الإخراج)، لُفّها في asyncio.gather: فثلاث انتظارات مدة كل منها ثانية تنتهي في ثانية واحدة إجمالًا، لا في ثلاث.
import asyncio
import time
async def fetch(name: str, delay: float) -> str:
await asyncio.sleep(delay)
return f"{name} done"
async def main() -> None:
start = time.perf_counter()
results = await asyncio.gather(
fetch("a", 1.0),
fetch("b", 1.0),
fetch("c", 1.0),
)
elapsed = time.perf_counter() - start
print(results)
print(f"total: {elapsed:.1f} s")
asyncio.run(main())افتراضيًا، تنشر gather أول استثناء يُطلَق، لكنها تترك المهام الأخرى تواصل العمل في الخلفية - وهذا نادرًا ما تريده. ومع return_exceptions=True، تُرجَع الاستثناءات كنتائج، لتفرزها بعد ذلك. ومنذ Python 3.11، فضّل asyncio.TaskGroup: فعند أول خطأ، تُلغى كل المهام الشقيقة بشكل نظيف، وتصعد الاستثناءات المتعددة مجمَّعة في ExceptionGroup، لتلتقطها بـ except*.
import asyncio
import time
def blocking_io() -> str:
time.sleep(1)
return "report written"
async def main() -> None:
results = await asyncio.gather(
asyncio.to_thread(blocking_io),
asyncio.sleep(1, result="tick"),
)
print(results)
asyncio.run(main())fetch('a', 1.0) مكتوبًا وحده، دون await ولا create_task؟asyncio.sleep(1) أُطلقت معًا عبر asyncio.gather: ما المدة الإجمالية؟time.sleep(2) داخل كوروتين؟