网络通信/QTcpSocket/QObject:Cannot create children for a parent that is in a different thread.

概述

在实现QTcpSocket多线程收发的实践中,当使用moveToThread把socket对象移动到次线程,然后再进行connectToHost操作时,遇到了如下告警(虽然执行结果上未见明显异常)。本文将从源码层次上分析产生这种告警的原因。
//QObject: Cannot create children for a parent that is in a different thread.
//(Parent is QTcpSocket(0x26e5de10), parent’s thread is QThread(0x72fd48), current thread is QThread(0x247c38a0)

场景复现

将TCP套接字对象移动到"发送线程"中,结果遇到了如上告警(只是告警并无实际运行异常)。我们以最小化代码复现当时情形。以网络调试助手为TCP服务端,构建客户端代码如下:

Widget::Widget(QWidget *parent)
    : QWidget(parent), ui(new Ui::Widget)
{
    ui->setupUi(this);

    m_pTcpSocket = new QTcpSocket();
    m_pTcpSocket->moveToThread(&m_Thread);

	//The connection type is determined when the signal is emitted. /so it's QueuedConnection
    QObject::connect(m_pTcpSocket, SIGNAL(connected()), this, SLOT(slot_connected()));
	//次线程启动
    m_Thread.start();
    //print
    qDebug() << "Main ThreadObj:" << this->thread() << "Child ThreadObj:" << &m_Thread << "TcpSocketObj:" << m_pTcpSocket;
}
//QueuedConnection //执行在主线程
void Widget::slot_connected()
{ qDebug() << QString("slot_connected ThreadID:") << QThread::currentThreadId(); }

//主线程连接按钮操作
void Widget::on_pushButton_connect_clicked()
{   m_pTcpSocket->connectToHost("127.0.0.1", 8091);  }

启动程序并执行链接按钮槽函数,程序在执行connectToHost的过程中,便出现了上述告警提示。某次运行结果如下:

//Main ThreadObj: QThread(0x247c38a0) Child ThreadObj: QThread(0x72fd48) TcpSocketObj: QTcpSocket(0x26e5de10)
//QObject: Cannot create children for a parent that is in a different thread.
//(Parent is QTcpSocket(0x26e5de10), parent's thread is QThread(0x72fd48), current thread is QThread(0x247c38a0)
//QObject: Cannot create children for a parent that is in a different thread.
//(Parent is QTcpSocket(0x26e5de10), parent's thread is QThread(0x72fd48), current thread is QThread(0x247c38a0)

同一份告警信息,报告了两次,内容完全一致。我们可以理解这个告警如下:
不能为隶属其他线程的父对象在当前线程中创建子对象。
不知名的xxx对象,其父对象是QTcpSocket类型,地址是0x26e5de10,也就是m_pTcpSocket。m_pTcpSocket 是属于 QThread(&m_Thread #0x72fd48) 这个次线程的(因为我们对齐使用了moveToThread),但是当前的操作线程却是 QThread(0x247c38a0),也即主线程。

源码分析

以Qt全源码目录为搜索范围,可查到告警信息的相关源码,是出自qobject.cpp中的 QObject static member functions

// check the constructor's parent thread argument
static bool check_parent_thread(QObject *parent,
                                QThreadData *parentThreadData,
                                QThreadData *currentThreadData)
{
    if (parent && parentThreadData != currentThreadData) {
        QThread *parentThread = parentThreadData->thread;
        QThread *currentThread = currentThreadData->thread;
        qWarning("QObject: Cannot create children for a parent that is in a different thread.\n"
                 "(Parent is %s(%p), parent's thread is %s(%p), current thread is %s(%p)",
                 parent->metaObject()->className(),
                 parent,
                 parentThread ? parentThread->metaObject()->className() : "QThread",
                 parentThread,
                 currentThread ? currentThread->metaObject()->className() : "QThread",
                 currentThread);
        return false;
    }
    return true;
}

使用自主编译库进行调试,将调试断点加在qWarning位置上,重新运行,分别记录两次告警触发时的函数调用堆栈。

堆栈分析告警的触发原因

在场景复现中,我们捕获到了两次告警。两次告警的内容完全一致,因为它们同是来自 QObject 类实现的check_parent_thread检查过程。猜测在connectToHost执行过程中有两个地方违反了这一约定,我们将断点加在 产生告警的qWarning语句上,再次运行程序,依次捕获两次出现告警时的函数堆栈。(此调试过程,必须使用自主编译的Qt库)

第一处提示来源于

在这里插入图片描述

//级别6的堆栈位置 //
socketEngine = QAbstractSocketEngine::createSocketEngine(q->socketType(), proxyInUse, q);

//级别4的堆栈位置
QNativeSocketEngine::QNativeSocketEngine(QObject *parent)
    : QAbstractSocketEngine(*new QNativeSocketEnginePrivate(), parent)  //位置处
{
}

//级别2的堆栈位置
QObject::QObject(QObjectPrivate &dd, QObject *parent)
    : d_ptr(&dd)
{
    Q_D(QObject);
    d_ptr->q_ptr = this;
    d->threadData = (parent && !parent->thread()) ? parent->d_func()->threadData : QThreadData::current();
    d->threadData->ref();
    if (parent) {
        QT_TRY {
            if (!check_parent_thread(parent, parent ? parent->d_func()->threadData : 0, d->threadData))

级别6的函数堆栈显示,在 connectToHost 函数的底层实现过程中,创建了一个 socketEngine 对象,它指定了 q指针(即m_pTcpSocket) 做parent 父对象。其问题在于,connectToHost 执行在主线程,因此在其执行下的 socketEngine对象属于主线程,而将 &m_pTcpSocket 这个次线程的对象指给它做父对象,这是不被 QObject 规则允许的,最终触发了QObject中的检查告警。

第二处提示来源于

第二次告警时的函数调用堆栈(将断点加在上述静态函数的告警信息提示出):
在这里插入图片描述

void QAbstractSocketPrivate::_q_connectToNextAddress()
{
...
        // Start the connect timer.
        if (threadData->hasEventDispatcher()) {
            if (!connectTimer) {
                connectTimer = new QTimer(q);  //函数堆栈中的代码行
                QObject::connect(connectTimer, SIGNAL(timeout()),
                                 q, SLOT(_q_abortConnectionAttempt()),
                                 Qt::DirectConnection);
            }
...
}

其中,connectTimer = new QTimer(q); 代码行中的q指针,在此处它是QAbstractSocket的对象。创建了一个定时器对象 connectTimer(属于主线程),它使用q(即m_pTcpSocket)做parent父对象。

explicit QTimer(QObject *parent = Q_NULLPTR);

在主线程中执行了QTimer对象创建,但是 parent 对象却不是主线程的,而是次线程的,因而触发QObject中的检查告警。

总结

我们不能贸然的将套接字 m_pTcpSocket对象 movetothread 到一个次线程m_pThread中,因为在类似QAbstractSocket::connectToHost 这样的成员函数中,会创建 “以m_pTcpSocket为父对象的对象”,被创建的对象属于最上层的调用者线程(Here是主线程),与为其指定的父对象所隶属的线程不一致,违反了QObject的创建规则。
但也不用过于小心,
并不是不可以将 m_pTcpSocket 移动到子线程,而是,如果你这么做了,那么相应的 connectToHost 等操作过程,也应该放到该次线程下执行。只需要简单的通过信号槽实现一种跨线程转换即可。比如下面的几个方法:
P1、将 connectToHost 操作封装到一个槽函数中,然后将其使用Qt::DirectConnection直接连接到 QThread::start 信号上。这种方法的本质类似:C语言多线程编程形式下,在入口函数的while循环之前,进行必要的初始化操作。但受限于start信号操作和执行时机,不太好支持太复杂的重新连接等功能。
P2、重载 QTcpSocket 类,并在其内部定义一个包含 connectToHost 过程的槽函数,在Widget类中定义一个信号,使用Qt::QueuedConnection 模式将它们连接起来。
P3、个人常用的 “线程代理对象 proxyObject”,通常适用于连接两个非QObject对象,在其中定义包含交互双方对象指针的信号和槽,队列连接它们,并封装信号为可跨线程调用接口。该方案并不适用于此场景,那会导致相同的告警,只不过现象成了,试图在次线程中创建“以主线程对象”为父的对象。
P4、应该还有不少其他方案,本文不再描述,本文重点关注标题中告警产生的原因。关于QTcpSocket编程实践、如何避免采坑等,可参见其它相关文章。

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值