AzLearn

أساسيات asyncio

asyncio Basics

مفهوم ~22 دقيقة

أساسيات asyncio — asyncio Basics

تخيّل أنك في مطعم وطلبت أكلاً يحتاج عشر دقائق للتحضير. في النموذج التسلسلي التقليدي، النادل يقف أمام المطبخ ينتظر دون أن يفعل شيئاً — لا يستقبل أحداً ولا يخدم طاولة أخرى. أما في نموذج asyncio، النادل يسجّل طلبك ثم يذهب لخدمة طاولات أخرى، ويعود إليك فور أن تصرّح المطبخ باكتمال طلبك.

هذا بالضبط ما يفعله asyncio في Python: يتيح للبرنامج الانتظار بكفاءة — بدلاً من تجميد كل شيء أثناء انتظار عملية I/O بطيئة (شبكة، ملف، قاعدة بيانات)، يُعلّق العملية الحالية وينفّذ عمليات أخرى في نفس الوقت.

ما هي حلقة الأحداث؟

حلقة الأحداث (Event Loop) هي قلب asyncio. تخيّلها مدير يراقب قائمة مهام:

  1. يبدأ مهمة غير متزامنة
  2. عندما تقول المهمة “أنا أنتظر شيئاً” (باستخدام await)، يُعلّقها
  3. ينتقل لمهمة أخرى في القائمة
  4. عندما ينتهي ما كانت المهمة الأولى تنتظره، يُعيد تشغيلها من حيث توقفت

كل هذا يحدث في خيط تنفيذ واحد (single thread). لا تعدد حقيقي في المعالجة — بل تبديل ذكي بين المهام في لحظات الانتظار.

async def و await

لتعريف دالة غير متزامنة، استخدم async def بدلاً من def العادية:

async def fetch_data():
    # هذه دالة coroutine — This is a coroutine function
    return "البيانات جاهزة"

الكلمة await تقول لحلقة الأحداث: “أنا أنتظر هذه العملية — يمكنك تشغيل غيري الآن”. لا يمكن استخدام await إلا داخل دالة async def.

async def main():
    result = await fetch_data()  # انتظر نتيجة fetch_data
    print(result)

مثال مباشر: asyncio.sleep

asyncio.sleep هو نظير time.sleep لكنه غير متزامن — عند استدعائه لا يُجمّد الخيط كله، بل يتركه لينفّذ مهام أخرى:

main.go

لاحظ أن المهمتين تعملان بالتسلسل هنا لأننا await-ننا واحدة ثم الثانية. في الدرس القادم ستتعلم كيف تشغّلهما بالتوازي.

متى يكون asyncio الاختيار الصحيح؟

قبل أن تستخدم asyncio في كل مكان، افهم قيوده:

asyncio مثالي لـ (I/O-bound):

  • طلبات HTTP وواجهات برمجية (APIs)
  • قراءة وكتابة الملفات
  • قواعد البيانات والعمليات الشبكية
  • تطبيقات الويب التي تخدم آلاف الطلبات المتزامنة

asyncio لا يفيد في (CPU-bound):

  • الحسابات الرياضية المعقدة
  • معالجة الصور والفيديو
  • التشفير وفك التشفير المكثف
  • أي عمل يحتاج قوة معالج حقيقية

السبب: asyncio يعمل في خيط واحد. عملية CPU-bound ستحتل الخيط بالكامل ولن تترك وقتاً لأي مهمة أخرى — كأنك وضعت نادلاً أمام الميكروويف يراقبه دون توقف.

أنواع الكائنات في asyncio

Coroutine: ما تعيده دالة async def عند استدعائها. لا تُنفَّذ وحدها — تحتاج await أو حلقة الأحداث.

async def greet():
    return "مرحبا"

# هذا يُنشئ coroutine لكن لا ينفّذها
coro = greet()

# هذا ينفّذها
result = await greet()

Awaitable: أي كائن يمكن استخدام await معه — يشمل coroutines والـ tasks والـ futures.

أخطاء شائعة مع async

الخطأ الأول: نسيان await

async def main():
    # ❌ خطأ — هذا لا يُنفّذ slow_task
    slow_task("مهمة", 1)  # يُنشئ coroutine فقط، لا ينفّذها

    # ✅ صواب
    await slow_task("مهمة", 1)

Python لن تُخطئك على هذا في بعض الأحيان، لكن المهمة لن تُنفَّذ. يمكن تفعيل تحذيرات asyncio لاكتشاف هذا.

الخطأ الثاني: استخدام time.sleep بدلاً من asyncio.sleep

import time
import asyncio

async def main():
    # ❌ خطأ — يُجمّد الخيط كله
    time.sleep(2)

    # ✅ صواب — يُعلّق هذه المهمة فقط
    await asyncio.sleep(2)

time.sleep يجمّد الخيط بأكمله — كأنك أوقفت النادل عن كل شيء. asyncio.sleep يُعلّق المهمة الحالية فقط ويسمح لغيرها بالعمل.

الخطأ الثالث: استدعاء دوال blocking داخل async

async def bad_example():
    # ❌ هذا يُجمّد حلقة الأحداث
    with open("large_file.txt") as f:
        data = f.read()  # عملية blocking

    # ✅ الحل: استخدم مكتبات async مثل aiofiles
    # import aiofiles
    # async with aiofiles.open("file.txt") as f:
    #     data = await f.read()

مثال عملي: محاكاة طلبات متعددة

main.go

مقارنة: المتزامن مقابل التسلسلي

لفهم القيمة الحقيقية لـ asyncio، قارن السلوكين:

import asyncio
import time

async def task(name, delay):
    await asyncio.sleep(delay)
    return f"{name} انتهت"

# تسلسلي: ~3 ثواني
async def sequential():
    r1 = await task("أ", 1)
    r2 = await task("ب", 1)
    r3 = await task("ج", 1)

# متوازٍ: ~1 ثانية (سيأتي في الدرس القادم)
async def parallel():
    r1, r2, r3 = await asyncio.gather(
        task("أ", 1),
        task("ب", 1),
        task("ج", 1),
    )

الفرق ليس في قوة المعالج — بل في استغلال وقت الانتظار. بينما تنتظر “أ”، نبدأ “ب” و"ج". كلها تنتظر في نفس الوقت، فتنتهي كلها معاً.

ملخص

المفهومالشرح
async defتُعرّف دالة coroutine
awaitتُعلّق المهمة الحالية وتنتظر نتيجة
asyncio.sleepانتظار كفء (لا يجمّد الخيط)
حلقة الأحداثالمدير الذي يُنسّق بين المهام
I/O-boundمناسب لـ asyncio
CPU-boundغير مناسب لـ asyncio
تحدي — Challenge