Interlacing ContextManager

@gcewing @ncoghlan

Regarding interrupt safety of Python’s own Lock implementation, I found some interesting facts:

1. Lock().__enter__() and Lock().__exit__() seems to be internal C bindings that are immune to interrupts.

2. Wrappers around the lock (including Python’s own Condition variables) are vulnerable to KeyboardInterrupt.

Here is the code I used for test:

from threading import Lock, Condition
from sys import stdout, stderr

class WrappedLock:
    def __init__(self):
        self._lock = Lock()

    def locked(self):
        return self._lock.locked()

    def __enter__(self):
        return self._lock.__enter__()

    def __exit__(self, *args):
        # SIGINT here will cause unreleased lock (essentially a deadlock)
        return self._lock.__exit__(*args)

lock1 = Lock()
lock2 = WrappedLock()
lock3 = Lock()
cond3 = Condition(lock3)

try:
    while True:
        with lock1, lock2, cond3:
            # Ask dispatcher to send SIGINT
            stdout.write("\n")
            stdout.flush()
except KeyboardInterrupt:
    pass

if lock1.locked():
    stderr.write("lock1\n")
if lock2.locked():
    stderr.write("lock2\n")
if lock3.locked():
    stderr.write("lock3\n")

I used a custom script to run this test in batches. You can find the code in my Github Gist.

I ran only 10000 tests because it already made my MacBook’s fan roaring at me. Please feel free to run more tests on a more powerful machine. I am also curious if this result is reproducible on other OS/Architectures.

Here are the results:

# Python 3.12.5 on MacOS
Total 10000 tests                                                                                                                                                                                                                                                                                                                                                                       
lock1 failed 0 times (0.00%)
lock2 failed 430 times (4.30%)
lock3 failed 514 times (5.14%)

This result shows that ContextManager works (and ONLY works) with raw, unwrapped Python Lock primitives to provide robust interrupt handling. I guess this will cause some very intricate confusions if someone assumes ContextManagers are always immune to interrupts.