当QT中使用继承QObject类,通过moveToThread方法实现多线程时,如果线程处理函数是处理UDP接收数据时,可能会出现无法接收到readyRead信号,或者只能接收1次readyRead信号的问题。
如代码所示:
该线程类的实现设想为:通过继承OBject类,自定义了一个UdpThread的线程类,在类属性中,有1个QUdpSocket和1个QThread成员属性,通过构造函数中将UdpThread类自身通过moveToThread方法启动多线程。再连接QUdpSocket的readyRead信号和自定义类UdpThread类的槽函数slotReadyRead来进行UDP数据的实时处理,slotReadyRead同时也是线程处理函数。
头文件代码
// 头文件 #ifndef UDPTHREAD_H #define UDPTHREAD_H #include <QObject> #include <QThread> #include <QUdpSocket> class UdpThread : public QObject { Q_OBJECT public: explicit UdpThread(QObject *parent = nullptr); void bind(QString ip, qint32 port); public slots: void slotReadyRead(); signals: void send(QByteArray* data); private: QThread _thread; QUdpSocket _socket; }; #endif // UDPTHREAD_H
源文件代码
// UdpThread.cpp源文件 #include "udpthread.h" UdpThread::UdpThread(QObject *parent) : QObject(parent) { this->moveToThread(&_thread); _thread.start(); connect(&_socket, &QUdpSocket::readyRead, this, &UdpThread::slotReadyRead); // 此时使用默认的连接方式Qt::QueuedConnection,即用于多线程的队列连接方式 } void UdpThread::bind(QString ip, qint32 port) { _socket.bind(QHostAddress(ip), port); } void UdpThread::slotReadyRead() { while(_socket.hasPendingDatagrams()) { QByteArray* datagram = new QByteArray; datagram->resize(_socket.pendingDatagramSize()); _socket.readDatagram(datagram->data(), datagram->size()); emit send(datagram); } }
此时,我们使用的时QT中多线程时的默认连接方式 Qt::QueuedConnection,此时在实例化后,readyRead信号只触发了一次,就中断了,通过调试时发现时因为调用QUdpSocket类的readDatagram函数时,QT会报一个错误,具体错误如下
QSocketNotifier: Socket notifiers cannot be enabled or disabled from another thread

原因:
在类对象初始化时,所有初始化的属性均依赖于该对象所在的线程,所以当使用moveToThread时,socket对象已经有了依赖线程,所以出现上图的错误。
(对于QSocketNotifier类还没具体研究,有一个还未实践的思路:是否该错误中的步骤是否时在连接readyRead信号时出现的,那么是否可以先将对象通过moveToThread方法移入子线程,之后通过在主线程发送信号触发该对象在子线程中启动readyRead的绑定)
解决思路:
QT的Socket类中都有与文件描述符(类似于windows的句柄)类似的QSocketDescriptor类,通过描述符可以在其他线程或进程创建一个同样的Socket对象。
代码如下:
// 头文件 #ifndef UDP_H #define UDP_H #include <QObject> #include <QUdpSocket> #include <QNetworkDatagram> #include <QSocketDescriptor> class Udp : public QObject { Q_OBJECT public: explicit Udp(QObject *parent = nullptr); Udp(QSocketDescriptor sock, QObject *parent = nullptr); signals: private: QUdpSocket* p; // 注意,此处应该只能用指针,不能直接初始化用QUdpsocket mSocket }; #endif // UDP_H // 源文件 #include "udp.h" #include <QDebug> #include <QThread> #include <QSocketNotifier> Udp::Udp(QObject *parent) : QObject(parent) { } Udp::Udp(QSocketDescriptor sock, QObject *parent) { this->setParent(parent); // 初始化QUdpSocket的指针p p = new QUdpSocket(this); // 根据传入的socketDescriptor绑定到传入的socket p->setSocketDescriptor(sock); // 连接信号readyRead connect(p, &QUdpSocket::readyRead, this, [=](){\ while(p->hasPendingDatagrams()) { qDebug()<<p->receiveDatagram().data(); qDebug()<<"udp thread:"<<QThread::currentThread(); } }); } // 主函数 #include "widget.h" #include "udp.h" #include <QApplication> #include <QThread> int main(int argc, char *argv[]) { QApplication a(argc, argv); // 创建socket对象 QUdpSocket tSocket; // 绑定端口 tSocket.bind(QHostAddress("192.168.74.101"), 6000); // 创建子线程 QThread thread(&a); // 创建线程处理类对象,并将tSocket的socketDescriptor传入 Udp udp(tSocket.socketDescriptor()); // 将线程处理类对象传入子线程 udp.moveToThread(&thread); // 启动线程 thread.start(); return a.exec(); }
在上例中,有1个疑问:
1. Socket是在主函数也就是主线程中绑定监听端口的,之后将SocketDescriptor在外部通过构造函数传入,并在构造函数内初始化,按照依赖关系,这个指针p在使用传入的SocketDescriptor初始化时,不也应该属于主线程吗?为什么这样就可以。
另外,Socket可以不用在主函数绑定,将它放在Udp类中作为一个属性绑定,并将它的SocketDescriptor给p进行创建另一个socket也可以,示例代码如下:
// 头文件 #ifndef UDP_H #define UDP_H #include <QObject> #include <QUdpSocket> #include <QNetworkDatagram> #include <QSocketDescriptor> class Udp : public QObject { Q_OBJECT public: explicit Udp(QObject *parent = nullptr); signals: private: QUdpSocket mSocket; QUdpSocket* p; }; #endif // UDP_H // 源文件 #include "udp.h" #include <QDebug> #include <QThread> #include <QSocketNotifier> Udp::Udp(QObject *parent) : QObject(parent) { // 绑定端口 mSocket.bind(QHostAddress("192.168.74.101"), 6000); // 初始化QUdpSocket的指针p p = new QUdpSocket(this); // 根据传入的socketDescriptor绑定到传入的socket p->setSocketDescriptor(mSocket.socketDescriptor()); // 连接信号readyRead connect(p, &QUdpSocket::readyRead, this, [=](){\ while(p->hasPendingDatagrams()) { qDebug()<<p->receiveDatagram().data(); qDebug()<<"udp thread:"<<QThread::currentThread(); } }); } // 主函数 #include "widget.h" #include "udp.h" #include <QApplication> #include <QThread> int main(int argc, char *argv[]) { QApplication a(argc, argv); // 创建子线程 QThread thread(&a); // 创建线程处理类对象,并将tSocket的socketDescriptor传入 Udp udp; // 将线程处理类对象传入子线程 udp.moveToThread(&thread); // 启动线程 thread.start(); return a.exec(); }
那么既然在构造函数中进行socket对象不会报错,那么有个思路,即不通过socketDescriptor,而是直接使用在构造函数中初始化的socket对象是否也可以,通过尝试,上述代码只需要一个未初始化的属性p,并在构造函数中绑定也不会报错
该思路经过测试是正确可行的,但是注意,需要在moveToThread之前bind,如果先moveToThread再bind,会出现已有父线程的对象无法移动之类的错误警告
// 头文件 #ifndef UDP_H #define UDP_H #include <QObject> #include <QUdpSocket> #include <QNetworkDatagram> #include <QSocketDescriptor> class Udp : public QObject { Q_OBJECT public: explicit Udp(QObject *parent = nullptr); signals: private: QUdpSocket* p; }; #endif // UDP_H // 源文件 #include "udp.h" #include <QDebug> #include <QThread> #include <QSocketNotifier> Udp::Udp(QObject *parent) : QObject(parent) { // 初始化QUdpSocket的指针p p = new QUdpSocket(this);
// 绑定监听端口
p->bind(QHostAddress("192.168.74.101"), 6000);
// 连接信号readyRead
connect(p, &QUdpSocket::readyRead, this, [=](){
while(p->hasPendingDatagrams()) {
qDebug()<<p->receiveDatagram().data();
qDebug()<<"udp thread:"<<QThread::currentThread();
}
});
}
// 主函数
#include "widget.h"
#include "udp.h"
#include <QApplication>
#include <QThread>
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
// 创建子线程
QThread thread(&a);
// 创建线程处理类对象,并将tSocket的socketDescriptor传入
Udp udp;
// 将线程处理类对象传入子线程
udp.moveToThread(&thread);
// 启动线程
thread.start();
return a.exec();
}
还有一种方式未尝试,就是在类自身中创建线程,但未尝试,代码如下:
// 头文件 #ifndef UDP_H #define UDP_H #include <QObject> #include <QUdpSocket> #include <QNetworkDatagram> #include <QSocketDescriptor> #include <QThread> class Udp : public QObject { Q_OBJECT public: explicit Udp(QObject *parent = nullptr); signals: private: QUdpSocket* p; QThread mThread; }; #endif // UDP_H // 源文件 #include "udp.h" #include <QDebug> #include <QSocketNotifier> Udp::Udp(QObject *parent) : QObject(parent) { // 初始化QUdpSocket的指针p p = new QUdpSocket(this); // 绑定监听端口 p->bind(QHostAddress("192.168.74.101"), 6000); // 连接信号readyRead connect(p, &QUdpSocket::readyRead, this, [=](){ while(p->hasPendingDatagrams()) { qDebug()<<p->receiveDatagram().data(); qDebug()<<"udp thread:"<<QThread::currentThread(); } }); // 将自身移入线程 this->moveToThread(&mThread); //// 或者将socket移入线程 //p->moveToThread(&mThread); // 启动线程 mThread->start(); } // 主函数 #include "widget.h" #include "udp.h" #include <QApplication> #include <QThread> int main(int argc, char *argv[]) { QApplication a(argc, argv); // 创建子线程 QThread thread(&a); // 创建线程处理类对象,并将tSocket的socketDescriptor传入 Udp udp; // 将线程处理类对象传入子线程 udp.moveToThread(&thread); // 启动线程 thread.start(); return a.exec(); }
注意:
以下方法不一定正确,也能让该错误消失,但是否是真正的多线程,或者是否对接收有什么影响还未研究
将信号readyRead和槽函数slotReadyRead的连接方式从队列连接Qt::QueuedConnection改为Qt::BlockingQueuedConnection即可
Qt::QueuedConnection和Qt::BlockingQueuedConnection的区别
Qt::QueuedConnection:槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在线程。发送信号之后,槽函数不会立刻被调用,等到接收者的当前函数执行完,进入事件循环之后,槽函数才会被调用。多线程环境下一般用这个。
Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
浙公网安备 33010602011771号