Qt多线程IoDevice使用分析

引言

 这两天在群里看到一个老哥用QThread创建了一个子线程读取串口数据的代码。

void myThread::run()
{
	...
	QThread::msleep(100);
	m_port->waitForReadyRead(10);
	ret= m_port->read(buf,len);
	...
}

 我也写过读写串口的代码,但是没有用waitForReadyRead 这个函数。我问他这个函数有什么用,我没加也能正常读取啊,而且你上边加了QThread::msleep(100)了,为什么还要加这样一个阻塞函数。老哥也是初学者,就跟我说了现象,不加这个函数的话,下面的read无法读取到数据,但是为什么,他也不清楚。
 我一想,我的读写代码都是在主线程中实现的,而它的代码是跨线程的,可能这就是导致问题的原因吧。

代码

 于是我大致按照它的代码写了下,模拟了不用waitForReadyRead的这种情况。

mythread.h 中myThread类(子线程)

class myThread : public QThread
{
public:
    myThread();
    void run();
    QSerialPort *m_serial;
};

mythread.cpp

myThread::myThread()
{
    m_serial = new QSerialPort;
    if (false == m_serial->isOpen())
    {
        m_serial->setPortName("COM6");
        m_serial->setBaudRate(QSerialPort::Baud9600);
        m_serial->setDataBits(QSerialPort::Data8);
        m_serial->setParity(QSerialPort::NoParity);
        m_serial->setStopBits(QSerialPort::OneStop);
        m_serial->setFlowControl(QSerialPort::NoFlowControl);
        m_serial ->open(QIODevice::ReadWrite);
    }
}
void myThread::run()
{
	...
	int wcount = m_serial->write(cmd);
	QTimer::singleShot(1000, this, [=](){
        qDebug()<<"usableSize = "<<m_serial->bytesAvailable();
        const QByteArray data = m_serial->readAll();
        qDebug()<<"dataSize = "<<data.size()<<m_serial->bytesAvailable();
    });
	
}

mainwindow.cpp 构造函数

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    m_myThread = new myThread();
    m_myThread->start();
}
代码简介

 篇幅原因,代码不贴全了,简单介绍一下。
就是主线程(GUI线程)中创建一个子线程实例,子线程的构造函数中初始化串口对象,然后子线程的run(即启动函数)中执行串口读写操作。

解惑过程

 我运行了上面代码,也没有读取到数据。

	int wcount = m_serial->write(cmd);
	QTimer::singleShot(1000, this, [=](){
        qDebug()<<"usableSize = "<<m_serial->bytesAvailable();
        const QByteArray data = m_serial->readAll();
        qDebug()<<"dataSize = "<<data.size()<<m_serial->bytesAvailable();
    });

 执行的结果为,

	usableSize =  0
	dataSize =  0 0

并且输出中有一句,貌似是warning的打印,但似乎不影响程序的运行,
QObject: Cannot create children for a parent that is in a different thread. (Parent is QSerialPort(0x20c23a80), parent's thread is QThread(0x846fb0), current thread is QThread(0x20c239c0)

 我这边选择暂时不管这个warning。
 很奇怪,在我向串口写入命令1s之后,居然还没有读取到返回的数据。同样的写法在主线程中,根据串口参数,只需要不到百毫秒便可正常收到所有数据。
 于是我将1s时间延长至2s 5s,发现改无论多久,都不能接收到任何数据。
 于是我在写入命令后加了waitForReadyRead函数,发现果然可以读到数据了,但是数据常常有误。
 于是,我查文档,

Certain subclasses of QIODevice, such as QTcpSocket and QProcess, are asynchronous. This means that I/O functions such as write() or read() always return immediately, while communication with the device itself may happen when control goes back to the event loop.

QIODevice的某些子类(如QTcpSocket和QProcess)是异步的。这意味着诸如write()或read()这样的I/O函数总是立即返回,而当控制返回到事件循环时,与设备本身的通信可能发生。

 原来如此,IODevice的一些子类(其中应该包括串口),读写操作是异步的。也就是说,我write之后并没有马上写入设备,很有可能在返回事件循环时,才进行真正的数据写入。关于这点有一个函数可以验证,

	/* 我这边cmd长度是10 */
	int wcount = m_serial->write(cmd);
	/* bytesToWrite,返回等待写入的字节数 */
	qDebug()<<"wcount"<<wcount<<"writebuff"<<m_serial->bytesToWrite();

执行输出,wcount 10 writebuff 10,可以看到write的字节数和等待写入设备的字节数都是10,说明此时没有写入设备,那cmd存在哪里呢,我查阅文档,发现并没有明确地说明,但是可以根据一些话推出,在设备与QSerialPort之间,还有一个buffer(缓冲区)
 于是我在不加阻塞函数waitForReadyRead的情况下,在定时器连接的槽函数读取数据之前也加了bytesToWrite的打印,

	QTimer::singleShot(1000, this, [=](){
        qDebug()<<"usableSize = "<<m_serial->bytesAvailable()<<m_serial->bytesToWrite();
        const QByteArray data = m_serial->readAll();
        qDebug()<<"dataSize2 = "<<data.size()<<m_serial->bytesAvailable();
    });

神奇的事情发生了,m_serial->bytesToWrite()的值居然还是10,也就是说buffer中的数据还在,并没有写入到设备中
 此时,我感到非常疑惑,明明调用了write却在线程结束后也没有实际将数据写入串口设备。按文档中说的,当控制返回到事件循环时,与设备本身的通信可能发生。等等,事件循环、线程结束,还有一开始的线程warning,一下子多了许多未知的东西,但是我觉得我应该从线程结束和子线程的事件循环查起。
 果不其然,从QThread中查到了有关事件循环的东西,

By default, run() starts the event loop by calling exec() and runs a Qt event loop inside the thread.

the thread will exit after the run function has returned. There will not be any event loop running in the thread unless you call exec().

默认情况下,run()中会调用exec()来执行事件循环。否则线程结束后,run函数就返回,没有任何事件循环在子线程中。
 也就是说,我代码中myThread中的run是重载了默认的run,并且我没有执行exec来跑事件循环。所以直到子线程结束,也没能进入自己的事件循环中,所以异步的write最终也没能将数据写入设备。
 于是,我尝试着在我重载的run()末尾加入exec()启动子线程事件循环,果然,读到数据了。
 此时我回过头来,查关于waitForReadyRead的文档,

[virtual] bool QIODevice::waitForReadyRead(int msecs)
This function can operate without an event loop. It is useful when writing non-GUI applications and when performing I/O operations in a non-GUI thread.
If called from within a slot connected to the readyRead() signal, readyRead() will not be reemitted.
Reimplement this function to provide a blocking API for a custom device. The default implementation does nothing, and returns false.
Warning: Calling this function from the main (GUI) thread might cause your user interface to freeze.

 原来如此,这个函数就是建议你在没有事件循环的情况下使用,
并且在QIODevice的介绍中也可以看到,

Certain subclasses of QIODevice, such as QTcpSocket and QProcess, are asynchronous. This means that I/O functions such as write() or read() always return immediately, while communication with the device itself may happen when control goes back to the event loop. QIODevice provides functions that allow you to force these operations to be performed immediately, while blocking the calling thread and without entering the event loop. This allows QIODevice subclasses to be used without an event loop, or in a separate thread:
waitForReadyRead() - This function suspends operation in the calling thread until new data is available for reading.
waitForBytesWritten() - This function suspends operation in the calling thread until one payload of data has been written to the device.
waitFor…() - Subclasses of QIODevice implement blocking functions for device-specific operations. For example, QProcess has a function called waitForStarted() which suspends operation in the calling thread until the process has started.

waitFor系列的函数,就是阻塞线程,达到一个同步的效果。

再分析

 其实,到这一步,一开始waitForReadyRead的问题主线程子线程写法一样运行结果却不同的问题已经得到解释了。但是上面的warning还有waitForReadyRead的写法读到的数据经常会有问题。所以我对这两个点继续调查。

warning

QObject: Cannot create children for a parent that is in a different thread.
(Parent is QSerialPort(0x20c23a80), parent's thread is QThread(0x846fb0), current thread is QThread(0x20c239c0)

 大致意思是说我无法创建这样的一个子对象和它的父对象在不同的线程中的子对象。
 通过打印定位,我很快定位到,这句warning是下面的代码导致的,

    int wcount = m_serial->write(cmd);

 首先,这句代码没有显示地创建对象,其次就算write内部执行的时候有创建对象,难道不是和m_serial串口对象在同一个线程中?
 带着疑惑,我又翻开了文档,这一查,大吃一惊,原来

父线程拥有 QThread对象的所有权。

 也就是说,我是主线程中创建的子线程,所以子线程对象是属于主线程的。既然子线程对象属于主线程,那么子线程的成员都属于主线程。所以m_serial串口对象其实是存在主线程中,而我的操作是在跨线程操作。
 进一步查阅,发现

QThread::run()是整个子线程的入口,它里面创建的东西才是属于子线程的。

 这一点我也通过在子线程的构造函数中打印线程号,和run中打印线程号和各种对象的线程号打印确定了。
 既然是这样,那如果串口的write实现时,创建了对象,确实会出现warning说的情况,一个对象中的不同部分分布在不同线程。
 OK,那想消除这个warning也很简单,就是让串口对象在run内创建,或者write不在子线程(run)中调用。
 诶,到这里我又发现一个问题,既然myThread对象本身是属于父线程的,QTimer::singleShot()发送的信号,接受者是this,也就是线程本身,那singleShot()连接的槽中的函数的执行不就是在父线程的对象中?不还是在父线程中执行。
 通过打印验证,果然,slot中的线程打印还是父线程。也就是说,我搞了半天,子线程中就执行了一个write,还是write不完全的,其他东西都是在父线程中的。
 到这里,我觉得我整个对Qt线程的用法,都是有问题的。
 上网查询有关资料,发现了一篇对Qt多线程使用这方面讲的非常透彻的文章,
传送门
 他的使用方法很简单,定义一个普通的QObject派生类,然后将其对象move到QThread中。使用信号和槽时根本不用考虑多线程的存在。也不用使用QMutex来进行同步,Qt的事件循环会自己自动处理好这个。(这边究其原因,发送信号的对象和slot的所属对象处于不同线程时,默认的连接类型是queneConnect,也就是执行的槽函数是在接收对象所属的线程中的,所以可以大胆地使用)
 至于读出的数据异常的问题,我大胆地猜测就是跨线程操作不确定性导致的,因为我改为一个线程中操作时,就再也没有出现。

总结

  1. IODevice子类的很多读写操作是异步的。
  2. waitForReadyRead会阻塞线程,达到同步的效果(但是GUI线程中会阻塞GUI,使界面卡住)
  3. 子线程重载的时候注意事件循环的处理
  4. 子线程对象和对象的成员对象都是属于父线程的
  5. 不要跨线程操作QIODevice相关类
  6. Qt多线程使用方法,定义一个普通的QObject派生类,然后将其对象move到QThread中。使用信号和槽时根本不用考虑多线程的存在。也不用使用QMutex来进行同步,Qt的事件循环会自己自动处理好这个。
  • 15
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值