当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一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。

 



posted on 2021-09-16 22:57  大王背我来巡山®  阅读(1882)  评论(0)    收藏  举报