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

The example above shows the recommended way to use a lock: inside an async with block. On entry the block awaits acquire(); on exit (whether the block returned normally, raised an exception, or was cancelled) the lock is released automatically. There is no path out of the block that leaves the lock held.

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

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()