在Pyside中使用Qt线程

3
我不理解下面示例代码的输出,在这里找到。当前线程和工作线程具有相同的地址,这是如何可能的?
from PySide import QtCore

class Master(QtCore.QObject):
    command = QtCore.Signal(str)
    def __init__(self):
        QtCore.QObject.__init__(self)

class Worker(QtCore.QObject):
    def __init__(self):
        QtCore.QObject.__init__(self)
    def do_something(self, text):
        print('in thread {} message {}'.format(QtCore.QThread.currentThread(), text))

if __name__ == '__main__':
    app = QtCore.QCoreApplication([])
    print(QtCore.QThread.currentThread())
    # give us a thread and start it
    thread = QtCore.QThread()
    thread.start()
    print(thread)

    # create a worker and move it to our extra thread
    worker = Worker()
    worker.moveToThread(thread)

    # create a master object and connect it to the worker
    master = Master()
    master.command.connect(worker.do_something)

    # call a method of the worker directly (will be executed in the actual thread)
    worker.do_something('in main thread')

    # communicate via signals, will execute the method now in the extra thread
    master.command.emit('in worker thread')

    # start the application and kill it after 1 second
    QtCore.QTimer.singleShot(1000, app.quit)
    app.exec_()

    # don't forget to terminate the extra thread
    thread.quit()

输出:

<PySide.QtCore.QThread object at 0x0000000002537688>
<PySide.QtCore.QThread object at 0x0000000002537688>
in thread <PySide.QtCore.QThread object at 0x00000000025377C8> message in main thread
in thread <PySide.QtCore.QThread object at 0x0000000002537688> message in worker thread

是的!谢谢,但您没有解释内存块重用背后的机制,所以我会接受我的答案。 - elmattic
@Stringer 当然由你决定,但考虑到其他答案的更高详细度和准确性,我会接受它而不是让你的赏金无处可去。 - Rob Grant
@Robert Grant:好的,完成了。 - elmattic
@Stringer,你能否确认一下我在你的系统上最新发现的问题,即在obj.thread()后面加上del obj会改变包装器的地址吗?我认为至少在我的版本中存在一个bug(把线程作为任何对象的子对象都没有意义)。 - Antti Haapala -- Слава Україні
2个回答

8

没有“包装器被重用”,只是旧的包装器对象(已删除)恰好位于新的包装器对象的相同内存地址。

PySide使用shiboken库将QObject封装为Python对象。 shiboken文档中提到:

与任何Python绑定一样,基于|project|的绑定使用引用计数来处理包装器对象(包含C ++对象的Python对象,不要将其与包装的C ++对象混淆)。当引用计数达到零时,“Python垃圾收集器会删除包装器并尝试删除封装实例,但有时封装的C ++对象已经被删除,或者可能C ++对象在Python包装器超出范围并死亡之后不应该被释放,因为C ++已经负责封装实例。”

除此之外,CPython malloc实现(以及许多其他常见的malloc)经常会重复使用刚删除对象的内存地址分配给后续创建的对象,导致常见的陷阱:
>>> {} is {}
False
>>> id({}) == id({})
True

因此,活对象的地址/ID始终是不同的;曾经存在过但现在已死亡的对象的地址只是一个好奇心。
在第一种情况下,两个字典的引用都被保留,因此它们的ID在技术上是不同的,而在第二种情况下,左侧字典在左侧ID被调用后很快被删除,之后才创建右侧字典,并分配在完全相同的内存地址。
然而,这还有点更加复杂。当你调用currentThread()时,它会返回一个新的包装对象,用于该QThread,但除非旧的包装对象仍然存活(即在Python端引用了它),否则将返回旧的包装对象:因此,
QtCore.QThread.currentThread() is QtCore.QThread.currentThread() 

将始终为True!


现在,你得到2个地址的原因是
in thread <PySide.QtCore.QThread object at 0x00000000028EB888> message in main thread
in thread <PySide.QtCore.QThread object at 0x00000000028EB8C8> message in worker thread

再次强调,与包装器“未被重用”或被保留无关,同样的情况也可能发生在其他对象创建后,其引用被保留 - 也就是说,不能确定封装的对象是否一直存在。

然而,如果您保留对2个不同的QThread对象的引用,则它们的id()在其生命周期内将是不同的。


总的来说,你不应该依赖于shiboken包装对象的地址做任何有用的事情;相反,如果你在Python代码中自己持有引用,则is测试是有用的。
请注意,如果设置了objectNameQObject__str__将打印它; 因此,您可以通过执行以下操作轻松理解输出:
app = QtCore.QCoreApplication([])
QtCore.QThread.currentThread().setObjectName('main thread')

thread = QtCore.QThread()
thread.start()
thread.setObjectName('worker thread')

并且使用更清晰的print

print('in thread: "{}" - message: "{}"'
    .format(QtCore.QThread.currentThread().objectName(), text))

这些消息将是:

in thread: "main thread" - message: "in main thread"
in thread: "worker thread" - message: "in worker thread"

还有一种方法可以获取底层C++对象的地址,使用shiboken模块;这对于调试其他内容也很方便;不幸的是,默认情况下Ubuntu 14.10上未安装Python库或任何与shiboken相关的包;经过多次尝试,我最终设法手动安装它。

结果比我想象的更令人害怕:

app = QtCore.QCoreApplication([])
print(QtCore.QCoreApplication.instance().thread())
obj = QtCore.QObject()
print(obj.thread())
del obj
print(QtCore.QThread.currentThread())

打印:

<PySide.QtCore.QThread object at 0x7fb4a1149cc8>
<PySide.QtCore.QThread object at 0x7fb4a1149cc8>
<PySide.QtCore.QThread object at 0x7fb4a1149d08>

没错,当我删除QObject后,线程包装器的地址会发生变化!因此,让我们从Shiboken.shiboken中导入dump

from Shiboken.shiboken import dump

def info(obj):
    print(id(obj))
    print(dump(obj))

app = QtCore.QCoreApplication([])
info(QtCore.QCoreApplication.instance().thread())

obj = QtCore.QObject()
info(obj.thread())    
del obj

info(QtCore.QCoreApplication.instance().thread())

输出结果为

140323585370568
C++ address....... PySide.QtCore.QThread/0xe3d880 
hasOwnership...... 0
containsCppWrapper 0
validCppObject.... 1
wasCreatedByPython 0
parent............ <PySide.QtCore.QCoreApplication object at 0x7f9fa175a948>

140323585370568
C++ address....... PySide.QtCore.QThread/0xe3d880 
hasOwnership...... 0
containsCppWrapper 0
validCppObject.... 1
wasCreatedByPython 0
parent............ <PySide.QtCore.QObject object at 0x7f9fa175aa48>

140323585370696
C++ address....... PySide.QtCore.QThread/0xe3d880 
hasOwnership...... 0
containsCppWrapper 0
validCppObject.... 1
wasCreatedByPython 0

即,shiboken 使我们调用了 thread() 方法的最后一个对象成为我们线程的父对象。每当那个对象被删除(也就是调用了.thread()的最后一个对象),包装器也会被删除。这种行为非常可疑;我不确定它是否是一个错误,但这证明信任包装器对象的id是完全不可信的。

2

我认为在这种情况下使用QThread.currentThread()并期望得到有意义的结果是无效的。

据我所知,QThread不是线程,而是线程的包装器,我们看到的只是用于工作线程的重复使用的包装器。 如果我将前者的QThread.currentThread()替换为QCoreApplication.instance().thread(),则可以获得正确的输出:

<PySide.QtCore.QThread object at 0x00000000029D98C8>
<PySide.QtCore.QThread object at 0x00000000029D9908>
in thread <PySide.QtCore.QThread object at 0x00000000029D98C8> message in main thread
in thread <PySide.QtCore.QThread object at 0x00000000029D9908> message in worker thread

原因非常简单,前者的调用不会保留对QThread包装器的引用,而QCoreApplication.instance().thread()则会保留引用。我们可以通过修改原始示例进行验证:

main_thread = QtCore.QThread.currentThread() # holds the reference
print(main_thread)
# give us a thread and start it
thread = QtCore.QThread()
print(thread)

现在的输出:

<PySide.QtCore.QThread object at 0x00000000028EB888>
<PySide.QtCore.QThread object at 0x00000000028EB8C8>
in thread <PySide.QtCore.QThread object at 0x00000000028EB888> message in main thread
in thread <PySide.QtCore.QThread object at 0x00000000028EB8C8> message in worker thread

编辑:

通过重复使用包装器,我的意思是第二个包装器重复使用存储第一个包装器的内存块,这就是为什么包装器具有相同的内存地址。

但我感兴趣的是背后的机制。原因是Python使用PyMalloc处理小对象(从Python 2.3开始默认启用),但也完全可以是glibc分配器本身的副作用(在现代分配器如jemalloc或tcmalloc和使用--without-pymalloc标志编译的Python中):

http://www.evanjones.ca/memoryallocator/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接