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的事件循環會自己自動處理好這個。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章