Home > Software design >  Is it safe to have a thread still running a member function of a context manager after __exit__?
Is it safe to have a thread still running a member function of a context manager after __exit__?

Time:08-05

In the following minimal example, bar continues to run after the scope of the with statement:

import threading
import time


class Foo:
    def __init__(self, delay):
        self._delay = delay
        pass

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        pass

    def bar(self):
        time.sleep(self._delay)
        print("done")


with Foo(2) as my_foo:
    thread = threading.Thread(target=my_foo.bar)
    thread.start()

time.sleep(3)

It seems to work, but is it guaranteed to be safe to do so, or can it lead to undefined behavior or errors (something with dangling references or similar)?

CodePudding user response:

The context manager has nothing with object creation/deletion, it just guarantee us __exit__ will be called.

In such cases it can be helpfull to see the bytecode:

import dis

class Foo:
    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        pass

def f():
    with Foo():
        pass

dis.dis(f)
 13           0 LOAD_GLOBAL              0 (Foo)
              2 CALL_FUNCTION            0
              4 SETUP_WITH              16 (to 22) 
              6 POP_TOP

 14           8 POP_BLOCK
             10 LOAD_CONST               0 (None)
             12 DUP_TOP
             14 DUP_TOP
             16 CALL_FUNCTION            3 # calling __exit__
             18 POP_TOP
             20 JUMP_FORWARD            16 (to 38) # if there is no exception just go to the end
        >>   22 WITH_EXCEPT_START
             24 POP_JUMP_IF_TRUE        28
             26 RERAISE
        >>   28 POP_TOP
             30 POP_TOP
             32 POP_TOP
             34 POP_EXCEPT
             36 POP_TOP
        >>   38 LOAD_CONST               0 (None)
             40 RETURN_VALUE

There is SETUP_WITH which generally push __exit__ on the stack and do some preparation in case of exception. 16 calls the __exit__.

To sum up, you are right, until thread is running reference counter for Foo won't be zeroed, that's why python won't collect it.

  • Related