Confusion with weakref, __del__ and threading

Rhamphoryncus rhamph at gmail.com
Wed Jun 11 07:40:33 CEST 2008


On Jun 10, 8:15 pm, George Sakkis <george.sak... at gmail.com> wrote:
> I'm baffled with a situation that involves:
> 1) an instance of some class that defines __del__,
> 2) a thread which is created, started and referenced by that instance,
> and
> 3) a weakref proxy to the instance that is passed to the thread
> instead of 'self', to prevent a cyclic reference.
>
> This probably sounds like gibberish so here's a simplified example:
>
> ==========================================
>
> import time
> import weakref
> import threading
>
> num_main = num_other = 0
> main_thread = threading.currentThread()
>
> class Mystery(object):
>
>     def __init__(self):
>         proxy = weakref.proxy(self)
>         self._thread = threading.Thread(target=target, args=(proxy,))
>         self._thread.start()
>
>     def __del__(self):
>         global num_main, num_other
>         if threading.currentThread() is main_thread:
>             num_main += 1
>         else:
>             num_other += 1
>
>     def sleep(self, t):
>         time.sleep(t)
>
> def target(proxy):
>     try: proxy.sleep(0.01)
>     except weakref.ReferenceError: pass
>
> if __name__ == '__main__':
>     for i in xrange(1000):
>         Mystery()
>     time.sleep(0.1)
>     print '%d __del__ from main thread' % num_main
>     print '%d __del__ from other threads' % num_other
>
> ==========================================
>
> When I run it, I get around 950 __del__ from the main thread and the
> rest from non-main threads. I discovered this accidentally when I
> noticed some ignored AssertionErrors caused by a __del__ that was
> doing "self._thread.join()", assuming that the current thread is not
> self._thread, but as it turns out that's not always the case.
>
> So what is happening here for these ~50 minority cases ? Is __del__
> invoked through the proxy ?

The trick here is that calling proxy.sleep(0.01) first gets a strong
reference to the Mystery instance, then holds that strong reference
until it returns.

If the child thread gets the GIL before __init__ returns it will enter
Mystery.sleep, then the main thread will return from Mystery.__init__
and release its strong reference, followed by the child thread
returning from Mystery.sleep, releasing its strong reference, and (as
it just released the last strong reference) calling Mystery.__del__.

If the main thread returns from __init__ before the child thread gets
the GIL, it will release the only strong reference to the Mystery
instance, causing it to clear the weakref proxy and call __del__
before the child thread ever gets a chance.  If you added counters to
the target function you should see them match the counters of the
__del__ function.

Incidentally, += 1 isn't atomic in Python.  It is possible for updates
to be missed.



More information about the Python-list mailing list