[PyQt] subtle bug in PyQt in combination with Python garbage collector

Kovid Goyal kovid at kovidgoyal.net
Sat Aug 13 17:05:14 BST 2011


This bug has been present for a very long time. As a workaround in my projects, 
I disable the automatic garbage collector and run garbage collection manually
in the GUI thread via QTimer. Here's the code to do that:

class GarbageCollector(QObject):

    '''
    Disable automatic garbage collection and instead collect manually
    every INTERVAL milliseconds.

    This is done to ensure that garbage collection only happens in the GUI
    thread, as otherwise Qt can crash.
    '''

    INTERVAL = 5000

    def __init__(self, parent, debug=False):
        QObject.__init__(self, parent)
        self.debug = debug

        self.timer = QTimer(self)
        self.timer.timeout.connect(self.check)

        self.threshold = gc.get_threshold()
        gc.disable()
        self.timer.start(self.INTERVAL)
        #gc.set_debug(gc.DEBUG_SAVEALL)

    def check(self):
        #return self.debug_cycles()
        l0, l1, l2 = gc.get_count()
        if self.debug:
            print ('gc_check called:', l0, l1, l2)
        if l0 > self.threshold[0]:
            num = gc.collect(0)
            if self.debug:
                print ('collecting gen 0, found:', num, 'unreachable')
            if l1 > self.threshold[1]:
                num = gc.collect(1)
                if self.debug:
                    print ('collecting gen 1, found:', num, 'unreachable')
                if l2 > self.threshold[2]:
                    num = gc.collect(2)
                    if self.debug:
                        print ('collecting gen 2, found:', num, 'unreachable')

    def debug_cycles(self):
        gc.collect()
        for obj in gc.garbage:
            print (obj, repr(obj), type(obj))

Kovid.


On Sat, Aug 13, 2011 at 01:11:38PM +0200, Erik Janssens wrote:
> Hello Phil,
> 
> I believe to have found a subtle bug in PyQt in
> combination with the Python garbage collector.
> 
> Here is what I think that happens :
> 
> - A QObject is constructed in one thread,
>   and this QObject construction contains a cyclic
>   dependency.
> 
> - this construction will not be deleted with
>   reference counting, but only when the garbage
>   collector is triggered
> 
> - now, if the garbage collector starts collecting
>   in a different thread then the one that the 
>   QObject was constructed in, the QObject gets
>   destroyed in that different thread
> 
> - when a QObject is destroyed, it sends events to
>   its parent object.  these events are then send in
>   the wrong thread
> 
> - this leads to corruption and/or segmentation faults
> 
> To see this happening, please run the attached test 
> case with a development build of Qt (because then,
> you see the assertion failure).
> 
> I have observed this with :
> 
> - Qt 4.7.2
> - PyQt 4.8.3
> - sip 4.12.1
> 
> PySide suffers from the same behavior.
> 
> Thank you and best regards,
> 
> Erik
> 
> 
> 
> 
> !DSPAM:3,4e465c069887238618555!

> """Test the behaviour of the qt bindings in various circumstances.
> """
> 
> import unittest
> 
> from PyQt4 import QtGui, QtCore
> 
> class GarbageCollectionCase( unittest.TestCase ):
>     
>     def setUp(self):
>         self.application = QtGui.QApplication.instance()
>         if not self.application:
>             import sys
>             self.application = QtGui.QApplication(sys.argv)
>         
>     def test_cyclic_dependency( self ):
>         """Create 2 widgets with a cyclic dependency, so that they can
>         only be removed by the garbage collector, and then invoke the
>         garbage collector in a different thread.
>         """
>         import gc
>         
>         class CyclicChildWidget(QtGui.QWidget):
>             
>             def __init__( self, parent ):
>                 super( CyclicChildWidget, self ).__init__( parent )
>                 self._parent = parent
>                 
>         class CyclicWidget(QtGui.QWidget):
>             
>             def __init__( self ):
>                 super( CyclicWidget, self ).__init__()
>                 CyclicChildWidget( self )
>                     
>         # turn off automatic garbage collection, to be able to trigger it
>         # at the 'right' time
>         gc.disable()
>         alive = lambda :sum( isinstance(o,CyclicWidget) for o in gc.get_objects() )
>         #
>         # first proof that the wizard is only destructed by the garbage
>         # collector
>         #
>         cycle = CyclicWidget()
>         self.assertTrue( alive() )
>         del cycle
>         self.assertTrue( alive() )
>         gc.collect()
>         self.assertFalse( alive() )
>         #
>         # now run the garbage collector in a different thread
>         #
>         cycle = CyclicWidget()
>         del cycle
>         self.assertTrue( alive() )
> 
>         class GarbageCollectingThread(QtCore.QThread):
>             
>             def run(thread):
>                 self.assertTrue( alive() )
>                 # assertian failure here, and core dump
>                 gc.collect()
>                 self.assertFalse( alive() )
>                     
>         thread = GarbageCollectingThread()
>         thread.start()
>         thread.wait()

> #0  0x00821416 in __kernel_vsyscall ()
> #1  0x00e72941 in raise (sig=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:64
> #2  0x00e75e42 in abort () at abort.c:92
> #3  0x03c2c997 in qt_message_output (msgType=QtFatalMsg, buf=0x96ea6f8 "ASSERT failure in QCoreApplication::sendEvent: \"Cannot send events to objects owned by a different thread. Current thread 97870f0. Receiver '' (of type 'QWidget') was created in thread 9545fa8\", file "...) at global/qglobal.cpp:2282
> #4  0x03c2cb8d in qt_message (msgType=QtFatalMsg, msg=0x3dd75f4 "ASSERT failure in %s: \"%s\", file %s, line %d", ap=0x2d22494 "\304[\343\003\360\245n\t\017[\343\003]\001") at global/qglobal.cpp:2328
> #5  0x03c2cef3 in qFatal (msg=0x3dd75f4 "ASSERT failure in %s: \"%s\", file %s, line %d") at global/qglobal.cpp:2511
> #6  0x03c2c53a in qt_assert_x (where=0x3e35bc4 "QCoreApplication::sendEvent", what=0x96ea5f0 "Cannot send events to objects owned by a different thread. Current thread 97870f0. Receiver '' (of type 'QWidget') was created in thread 9545fa8", file=0x3e35b0f "kernel/qcoreapplication.cpp", line=349) at global/qglobal.cpp:2035
> #7  0x03d5542d in QCoreApplicationPrivate::checkReceiverThread (this=0x963e810, receiver=0x96dfe08) at kernel/qcoreapplication.cpp:349
> #8  0x0514e96c in QApplication::notify (this=0x9548db8, receiver=0x96dfe08, e=0x2d22914) at kernel/qapplication.cpp:3754
> #9  0x01635f02 in sipQApplication::notify (this=0x9548db8, a0=0x96dfe08, a1=0x2d22914) at sipQtGuiQApplication.cpp:317
> #10 0x03d56178 in QCoreApplication::notifyInternal (this=0x9548db8, receiver=0x96dfe08, event=0x2d22914) at kernel/qcoreapplication.cpp:731
> #11 0x05140e3d in QCoreApplication::sendEvent (receiver=0x96dfe08, event=0x2d22914) at ../../include/QtCore/../../src/corelib/kernel/qcoreapplication.h:215
> #12 0x051a781e in QWidget::~QWidget (this=0x96dfe08, __in_chrg=<value optimized out>) at kernel/qwidget.cpp:1647
> #13 0x01686d84 in sipQWidget::~sipQWidget (this=0x96dfe08, __in_chrg=<value optimized out>) at sipQtGuiQWidget.cpp:345
> #14 0x03d6f226 in QObjectPrivate::deleteChildren (this=0x96ad5e8) at kernel/qobject.cpp:1955
> #15 0x051a7788 in QWidget::~QWidget (this=0x96ad598, __in_chrg=<value optimized out>) at kernel/qwidget.cpp:1631
> #16 0x01686d84 in sipQWidget::~sipQWidget (this=0x96ad598, __in_chrg=<value optimized out>) at sipQtGuiQWidget.cpp:345
> #17 0x0169e40e in release_QWidget (sipCppV=0x96ad598, sipState=6) at sipQtGuiQWidget.cpp:9307
> #18 0x0169e4ca in dealloc_QWidget (sipSelf=0xb70edf5c) at sipQtGuiQWidget.cpp:9325
> #19 0x0094a30f in forgetObject (sw=0xb70edf5c) at siplib.c:10029
> #20 0x00949b30 in sipWrapper_dealloc (self=0xb70edf5c) at siplib.c:9580
> #21 0x00bd555a in subtype_dealloc (self=0xb70edf5c) at Objects/typeobject.c:1005
> #22 0x00bb6734 in dict_dealloc (mp=0xb709746c) at Objects/dictobject.c:985
> #23 0x009494d1 in sipSimpleWrapper_clear (self=0xb70f8d1c) at siplib.c:9150
> #24 0x00949a4c in sipWrapper_clear (self=0xb70f8d1c) at siplib.c:9531
> #25 0x00bd58f7 in subtype_clear (self=0xb70f8d1c) at Objects/typeobject.c:874
> #26 0x00c56ad6 in delete_garbage (generation=<value optimized out>) at Modules/gcmodule.c:769
> #27 collect (generation=<value optimized out>) at Modules/gcmodule.c:930
> #28 0x00c574af in gc_collect (self=0x0, args=0xb76d402c, kws=0x0) at Modules/gcmodule.c:1067
> #29 0x00bbabb8 in PyCFunction_Call (func=0xb70939ec, arg=0xb76d402c, kw=0x0) at Objects/methodobject.c:85
> #30 0x00c1f901 in call_function (f=0x96e1dc4, throwflag=0) at Python/ceval.c:4012
> #31 PyEval_EvalFrameEx (f=0x96e1dc4, throwflag=0) at Python/ceval.c:2665
> #32 0x00c21270 in PyEval_EvalCodeEx (co=0xb711c218, globals=0xb71162d4, locals=0x0, args=0xb7101558, argcount=1, kws=0x0, kwcount=0, defs=0x0, defcount=0, closure=0xb711daa4) at Python/ceval.c:3252
> #33 0x00ba0f17 in function_call (func=0xb70951b4, arg=0xb710154c, kw=0x0) at Objects/funcobject.c:526
> #34 0x00b7159c in PyObject_Call (func=0xb70951b4, arg=0xb710154c, kw=0x0) at Objects/abstract.c:2529
> #35 0x00b83484 in instancemethod_call (func=0xb70ef734, arg=0xb710154c, kw=0x0) at Objects/classobject.c:2578
> #36 0x00b7159c in PyObject_Call (func=0xb70ef734, arg=0xb76d402c, kw=0x0) at Objects/abstract.c:2529
> #37 0x00c19b94 in PyEval_CallObjectWithKeywords (func=0xb70ef734, arg=0xb76d402c, kw=0x0) at Python/ceval.c:3881
> #38 0x0093efc0 in sip_api_call_method (isErr=0x0, method=0xb70ef734, fmt=0x721031b "") at siplib.c:1742
> #39 0x0706df2a in sipVH_QtCore_11 (sipGILState=PyGILState_UNLOCKED, sipMethod=0xb70ef734) at sipQtCorecmodule.cpp:4376
> #40 0x070b8664 in sipQThread::run (this=0x97870f0) at sipQtCoreQThread.cpp:165
> #41 0x03c39fef in QThreadPrivate::start (arg=0x97870f0) at thread/qthread_unix.cpp:320
> #42 0x00fc1cc9 in start_thread (arg=0x2d23b70) at pthread_create.c:304
> #43 0x00f1869e in clone () at ../sysdeps/unix/sysv/linux/i386/clone.S:130
> 

> _______________________________________________
> PyQt mailing list    PyQt at riverbankcomputing.com
> http://www.riverbankcomputing.com/mailman/listinfo/pyqt
> 
> !DSPAM:3,4e465c069887238618555!


-- 
_____________________________________

Dr. Kovid Goyal 
http://www.kovidgoyal.net
http://calibre-ebook.com
_____________________________________
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 198 bytes
Desc: Digital signature
URL: <http://www.riverbankcomputing.com/pipermail/pyqt/attachments/20110813/1a505c12/attachment.pgp>


More information about the PyQt mailing list