参考链接
先说结论
不管是Direct Connections还是Queued Connections,我们都应该使用const &而不是使用值传递。
当使用值传递的时候,将至少发生一次数据拷贝,如果跨线程,则再发生一次。
当使用const &的时候,同线程一半不会拷贝,跨线程的时候将强制发生一次拷贝,因为引用在线程间传递是不安全的,QT会强制执行数据拷贝,将数据序列化到事件队列中。
当使用指针传递的时候,则指拷贝指针。
额外注意:使用自定义类型,需要使用qRegisterMetaType注册元类型,参考 Qt 线程间信号槽传递自定义数据类型 - Larpx的站点|一个什么都记录的小站
最佳实践
使用共享指针,即QSharedPointer进行传递,这样即使跨线程操作,也仅拷贝指针,减小系统开销。
使用const &.
优化程序结构,值传递的时候,仅传递基本类型、小对象。
参数传递细节解析
注:在QT5.15 MSVC 2022 环境下测试,MyData为class。
Direct Connections链接情况下:
操作类型1:
我们需要在类的复制构造函数和赋值运算符中设置断点,这样我们的程序只调用 ,断点就不会被命中。这说明这种传参的操作其实不会发生数据拷贝。其实这结果并不意外,因为这是 C++ 中作为 const & 传递参数的基本工作方式。Direct Connection链接方式只不过是一连串同步或直接的 C++ 函数调用。
下面是信号发出时,函数的调用链。
1. emit sendConstRef(c)
2. MainView::sendConstRef // generated by moc
3. QMetaObject::activate
4. MainView::qt_static_metacall // generated by moc
5. MainView::receiveConstRef第 2、3 和 4 步的元对象代码(分别用于对信号参数进行 marshalling、将发射的信号路由到连接的槽以及对槽的参数进行去 marshalling)的编写方式不会发生参数的数据拷贝。这样就只有两个地方可能会出现复制对象的情况:步骤1将对象传递给函数和步骤5槽函数读取对象.
但是这两个地方又受到标准 C++ 行为的约束,由于两个函数都传递的是常量引用,因此不需要复制数据,直接使用地址即可进行访问。同时对象也不存在生命周期问题,因为在信号发出和槽函数执行结束时,对象会在超出作用域之前返回。
操作类型2:
根据操作1中的分析,我们可以很容易地推断出在这种情况下,会发生一次数据拷贝。当qt_static_meta_call在步骤4中调用槽函数时,由于数据对象是按照值传递的,因此会执行一次拷贝操作。
操作类型3:
同样也会发生了一次值传递,在数据对象传递到信号的时候发生的。
操作类型4:
这种情况则会发生两次数据拷贝,第一次发生在数据对象传递到信号的时候,第二次发生在数据对象传递到槽函数的时候。
Queue Connections 链接情况下:
Queue Connections连接其实不过是异步函数调用而已。从概念上讲,路由函数QMetaObject::activate不再直接调用槽,
而是根据槽函数及其参数创建一个命令对象,并将该命令对象插入到事件队列中去。
当轮到命令对象执行的时候,事件循环的调度器便将该命令对象从队列中删除,并通过调用槽函数来去执行它。
当QMetaObject::activate创建命令对象时,它会在命令对象中存储参数对象的副本。因此对于每个信号槽的组合,都会有一个额外的副本。
这也就是为什么当我们跨线程使用信号传递自定义数据类型的时候,需要使用qRegisterMetaType在Qt的元对象系统中进行注册,需要注意的时候,这时候的自定义数据类型,需要具有默认构造函数、复制构造函数和析构函数,不然无法使用qRegisterMetaType注册,如果不进行注册,则会导致槽函数无法执行。
即使在多线程场景中,我们也应该通过const引用将参数传递给信号和槽,以避免不必要的参数复制。
评论