15  اللاتزامن

المشكلة: إطار starlette مبني ليكون لا تزامنيًّا (async). وهذه الكلمة تظهر كثيرًا في النصوص المرجعية؛ لذا وجب علينا بيان معنى اللاتزامنية.

تنقسم العمليات إلى نوعين:

عمليات حساب (Computation): وهي عمليات تنفذها وحدة المعالجة المركزية (CPU). وذلك مثل الجمع والطرح، وفصل النصوص ودمجها، الشرط والتكرار، والعد والمقارنة، ونحو ذلك.

وعمليات إدخال وإخراج (I/O): وهي عمليات تنفذها الأجهزة المرتبطة به؛ بناءً على طلبه. وذلك مثل:

  1. قراءة الملفات (من جهاز تخزين)
  2. أو إرسال في طلب بيانات عبر الشبكة (عبر منفذ الشبكة عبر أسلاك وموجهات ومحطات وأجهزة)

الحاجة إلى اللاتزامن

فأما عمليات الحساب فأسرع بآلاف وملايين المرات من عمليات الإدخال والإخراج. فنحن حين نرسل لخادم قاعدة البيانات فإن الأصل هو انتظار الإجراء نهاية العملية لينتقل للسطر الذي يليه. وهذا فيه إهدار لوقت المعالج بلا استغلال.

تخيل أن خوادم الويب تستقبل آلاف الطلبات في الدقيقة؛ وهي في نفسها عميل لخادم قاعدة البيانات؛ تُرسِلُ له طلبًا أو أمرًا وتنتظر الجواب منه، حتى تستطيع هي أن تكمل معالجتها وتجيب عميلها هي، وهو المستخدم.

آلية اللاتزامُن (async)؛ تمكن المعالج من حفظ السطر الذي توقف فيه بكلمة await بسبب إنشاء طلب لخادم آخر؛ ليخدم هو عملاء آخرين إلى حين وصول الجواب إليه. فهو يتفقَّدُ حالة الطلب كل فترة لكن لا يشغله ذلك عن استغلال وقته في خدمة العملاء الآخرين.

صورة: توضح الفرق في وقت الانتظار بين العمل وفق آلية تزامنية (Sync. I/O)، وآلية لا تزامنية (Async. I/O).

استعمال اللاتزامن مع قاعدة البيانات

سنقوم بتثبيت asyncpg سائق التواصل المقابل لـ psycopg لكنه أسرع منه بأضعاف في آلية اللاتزامن:

uv add SQLAlchemy asyncpg

نستعمل create_async_engine لإنشاء محرِّك يدعم الآلية اللاتزامنية في الاتصال والإرسال، ولاحظ وجود postgresql+asyncpg في بداية نص الاتصال:

from sqlalchemy.ext.asyncio import create_async_engine

engine = create_async_engine("postgresql+asyncpg://user123:password123@localhost:5432/dbname://", echo=True)

ثم نستعمله في دالة، ويجب أن توسَم بكونها لا تزامنية هي أيضًا بالكلمة async على النحو التالي:

from sqlalchemy import select

async def list_questions():
    async with engine.begin() as conn:
        query = select(questions_table)
        result = await conn.execute(query)
        rows = result.fetchall()
        return rows

وهذه الدوال لها طبيعة مختلفة عند الاستدعاء الأوَّل في بايثون، وذلك أن آلية اللاتزامن تتضمَّن تحكُّمًا أكثر في طريقة التنفيذ بمكتبة asyncio المبنيَّة في بايثون. ولن ندخل في تلك التفاصيل وإنما سنتركها لإطار starlette ليتولاها.

استعمال الآلية اللاتزامنية في خادم starlette

والأمر سهل في starlette؛ فكل ما نحتاج فعله هو وضع كلمة async قبل الدوال المتربطة بالمسارات، ثم وضع await عند استدعاء الدوال اللامتزامنة (وهي معرَّفة بـ async def)؛ على النحو التالي:

from starlette.responses import HTMLResponse, RedirectResponse
from starlette.templating import Jinja2Templates
from starlette.requests import Request


async def home(request: Request):
    questions = await list_questions()
    return templates.TemplateResponse("home.html",
        context={
            "request": request,
            "questions": questions,
        }
    )


routes = [
    Route("/", endpoint=home, methods=["GET"]),
]


app = Starlette(debug=True, routes=routes)