8.8. الأقفال

يوفر asyncio.Lock الإقصاء المتبادل بين الإجراءات المتزامنة (coroutines) -- أي ضمان أن إجراءً متزامنًا واحدًا فقط يحمل القفل في كل مرة، بينما ينتظر الآخرون حتى يحرره حامله.

8.8.1. متى تكون هناك حاجة إلى قفل

أشارت صفحة التزامن التعاوني إلى أن إجراءين متزامنين لا يمكن أن يتداخلا في منتصف شيفرة لا تحتوي على await. والعكس صحيح أيضًا: بمجرد أن ينفّذ إجراء متزامن await، تصبح الحلقة حرة في تشغيل إجراء متزامن آخر. وإذا تعامل إجراءان متزامنان مع المورد نفسه عبر عمليات الانتظار -- ناقل 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()