8.8. מנעולים#

asyncio.Lock מספק הדרה הדדית בין קורוטינות – ערובה לכך שרק קורוטינה אחת בכל רגע נתון מחזיקה במנעול, והשאר ממתינות עד שהמחזיק משחרר אותו.

8.8.1. מתי דרוש מנעול#

העמוד מקביליות שיתופית ציין ששתי קורוטינות אינן יכולות להשתלב באמצע קטע קוד שאין בו await. גם ההפך נכון: ברגע שקורוטינה מבצעת await, הלולאה חופשית להריץ קורוטינה אחרת. אם שתי קורוטינות נוגעות באותו משאב לאורך מספר awaits – אפיק UART, I2C או SPI – הפעולות שלהן עלולות להשתלב באופן שמשחית את המשאב.

מנעול סביב הקטע הקריטי סוגר את הפרצה:

import asyncio

bus_lock = asyncio.Lock()

async def read_register(bus, addr):
    async with bus_lock:
        bus.write(addr)
        return await bus.read(2)

כעת שתי קורוטינות יכולות שתיהן לקרוא ל-read_register במקביל; המנעול מבטיח שרק אחת מהן מחזיקה באפיק בכל רגע נתון, והשנייה ממתינה לשחרור המנעול לפני שהיא מתחילה.

מנעולים אינם דרושים כאשר אין await בתוך הקטע הקריטי – ערובת התזמון השיתופי כבר מכסה את המקרה הזה. הם דרושים רק כאשר הקטע הקריטי מוסר שליטה ללולאה באמצע הדרך.

8.8.2. מטבע הלשון async with#

הדוגמה לעיל מציגה את הדרך המומלצת להשתמש במנעול: בתוך בלוק async with. בכניסה הבלוק מבצע await ל-acquire(); ביציאה (בין אם הבלוק חזר כרגיל, זרק חריגה, או בוטל) המנעול משוחרר אוטומטית. אין מסלול יציאה מהבלוק שמשאיר את המנעול מוחזק.

עבור המקרה הנדיר שבו משך החיים של המנעול אינו מתיישר עם בלוק קוד, השיטות זמינות גם ישירות:

await bus_lock.acquire()
try:
    bus.write(addr)
    result = await bus.read(2)
finally:
    bus_lock.release()

ה-try/finally נדרש כדי שזה יהיה שקול לגרסת ה-async with. צורת ה-async with קיימת משום שזוהי הצורה הנכונה והשפה הופכת אותה לתמציתית.

8.8.3. מדריך השיטות#

  • acquire() – קורוטינה. חוסמת עד שהמנעול פתוח, ואז תופסת אותו.

  • release() – משחררת את המנעול. אם קורוטינות כלשהן בתור ממתינות על acquire(), הבאה בתור מתוזמנת לריצה והמנעול נשאר נעול; אחרת המנעול הופך לפתוח.

  • locked() – מחזירה True אם המנעול מוחזק כעת, False אחרת. מחזירה מיד; אינה חוסמת.

ממתינים מטופלים בסדר FIFO. אין עדיפות, אין כניסה חוזרת (אותה משימה אינה יכולה לתפוס מנעול שכבר ברשותה), ואין פסק זמן בעת התפיסה. כדי להציב מועד אחרון לתפיסת מנעול, עטפו את התפיסה ב-asyncio.wait_for()

try:
    await asyncio.wait_for(bus_lock.acquire(), timeout=1)
except asyncio.TimeoutError:
    # lock busy for >1 s -- bail out
    return None
try:
    ...
finally:
    bus_lock.release()