• בלוג
  • עמוד 2
  • בואו נזהה באגים בקוד פייתון אסינכרוני

בואו נזהה באגים בקוד פייתון אסינכרוני

29/09/2026

נכון כולנו כותבים פחות קוד בימים אלה אבל אני מקווה שקוראים הרבה יותר. הנה 4 באגים עם הסברים בקוד אסינכרוני בשפת פייתון ששווה ללמוד לזהות ואולי יעברו מתחת לרדאר אם לא תקראו את הקוד.

1. באג 1

הפונקציה הבאה מקבלת מזהה משתמש וטוענת את הנתונים כדי להציג לוח מחוונים לאותו משתמש:

async def dashboard(user_id):
    user     = await get_user(user_id)
    orders   = await get_orders(user_id)
    invoices = await get_invoices(user_id)

רואים איפה זה שבור?

נכון, הפונקציה אומנם כתובה עם async אבל היא סדרתית לגמרי. תחילה היא מושכת את נתוני המשתמש, לאחר מכן את נתוני ההזמנות ובסוף את החשבוניות. קוד אסינכרוני אמיתי ישתמש ב gather כדי להריץ את כל שלושת הפעולות במקביל:

async def dashboard(user_id):
    user, orders, invoices = await asyncio.gather(
        get_user(user_id),
        get_orders(user_id),
        get_invoices(user_id),
    )

    return user, orders, invoices

2. באג 2

גם כשהקוד מבלבל תמיד צריך לחפש את ה gather ולוודא שהיא מופיעה עבור כל סדרת פעולות שצריכה לקרות במקביל. רואים איפה זה נשבר כאן?

async def user_orders(uid):
    return [await get_order(uid, i) for i in range(3)]

await asyncio.gather(*(user_orders(u) for u in users))

למרות שיש קריאה ל gather זו קריאה חלקית. היא מכסה את הלולאה החיצונית אבל הלולאה הפנימית עדיין סדרתית, כל לולאה צריכה את ה gather שלה:

async def user_orders(uid):
    return await asyncio.gather(
        *(get_order(uid, i) for i in range(3))
    )

results = await asyncio.gather(
    *(user_orders(u) for u in users)
)

3. באג 3

אוקיי אוקיי עכשיו ה gather מתאים למספר הלולאות:

for i in range(0, len(urls), 10):
    await asyncio.gather(*(fetch(u) for u in urls[i:i+10]))

הקוד לוקח כל פעם עשר כתובות URL ומושך אותן במקביל כדי לא להעמיס על השרת. רואים מה הבעיה כאן?

בשביל לראות את זה אל תחשבו על המהירות של כל מנה בנפרד אלא של הרשימה כולה. אם יש שני URL-ים איטיים במנה מסוימת עדיין משתלם להוריד במקביל 8 נוספים במקום ה-8 שהסתיימו. בעצם הבחירה לחכות כל פעם למנה של עשר כתובות היא הטעות. פתרון טוב יותר יהיה להחזיק Semaphore של 10 בקשות פעילות:

sem = asyncio.Semaphore(10)

async def limited_fetch(url):
    async with sem:
        return await fetch(url)

results = await asyncio.gather(
    *(limited_fetch(u) for u in urls)
)

והאמת גם הקוד כולו יצא יותר קריא.

4. באג 4

בתוכנית האחרונה אנחנו רוצים להגביל את מספר הפעמים ש complex_calculation נקרא עבור פרמטר מסוים כי אנחנו יודעים שהפונקציה תמיד תחזיר את אותה תוצאה אם נעביר לה את אותו קלט. זה הקוד:

lock = asyncio.Lock()
cache = {}

async def get_cached(key):
    async with lock:
        if key not in cache:
            cache[key] = await complex_calculation(key)
        return cache[key]

# the caller
await asyncio.gather(*(get_cached(k) for k in ["a", "b", "c", "d", "e", "a", "a"]))

המטרה היא ש complex_calculation תופעל רק על הערכים השונים וכל ה a-ים יחכו לחישוב שפועל, כלומר שהיא לא תופעל 3 פעמים עבור שלושת המופעים של a.

אבל זה לא מה שקורה.

בגלל שהנעילה מכסה גם את הכתיבה למילון וגם את הפעלת החישוב כל אחד מהערכים מחושב בנפרד בצורה סדרתית. החישוב של a חוסם גם את b, c, d ו e.

מה עושים? משתמשים ב task ומפרידים בין החישוב לבין הנעילה:

lock = asyncio.Lock()
cache = {}

async def get_cached(key):
    async with lock:
        task = cache.get(key)

        if task is None:
            task = asyncio.create_task(complex_calculation(key))
            cache[key] = task

    return await task