Qt Socket多线程绑定解绑,QSocketNotifier: Socket notifiers cannot be enabled or disabled from another thread

在项目中,为了实现在UI界面上解绑和重新绑定UDP Socket,并避免阻塞UI,尝试在子线程处理数据接收。遇到的问题是Qt的UdpSocket.abort()方法在不同线程中调用引发线程不安全问题。解决方案是创建自定义类MyUdpSocket,封装一个槽函数来安全调用线程不安全的函数,确保Socket操作在同一线程中执行。代码示例包括myudpsocket.h/cpp、mythread.h/cpp以及UI线程调用部分。

问题描述:

项目上需要实现一个功能,利用UI界面点击实现Qt UdpSocket解除绑定和重新绑定的功能。由于Udp接收数据之后要执行复杂的计算逻辑,为了避免阻塞UI界面,需要在子线程执行UDP数据的接收和处理工作。然而,在UI界面所在的线程调用udpsocket->abort();会出现下面问题:

QSocketNotifier: Socket notifiers cannot be enabled or disabled from another thread
ASSERT failure in QCoreApplication::sendEvent: "Cannot send events to objects owned by a different thread. Current thread 0x0x1ccdc5a0. Receiver '' (of type 'QNativeSocketEngine') was created in thread 0x0x1e9f23e8", file kernel\qcoreapplication.cpp, line 576

问题分析:

经过查阅发现Qt的UdpSocket的abort方法是线程不安全的,我们在主线程使用UdpSocket的引用并调用abort方法是在UI线程当中执行的,由于UdpSocket是在继承QThread类的run函数当中构造的,二者属于不同的线程,导致问题的发生。

解决思路

由于UdpSocket的abort方法不是一个槽函数,无法通过信号槽的第五个参数来控制在哪个线程当中执行。继承QThread的类除了run函数内构造的东西,其他都属于主线程。为此,考虑修改或自定义一个类MyUdpSocket,构造一个槽函数,即可完成对于这种线程不安全函数的调用。
通过继承或者是通过成员变量的方法都可以实现上述方法,但是由于继承方式需要了解被继承类的具体结构,成本较高。这里使用成员变量的方式来实现封装。

代码如下

myudpsocket.h

#ifndef MYUDPSOCKET_H
#define MYUDPSOCKET_H

#include <QObject>
#include <QUdpSocket>
#include <QDebug>

class MyUDPSocket : public QObject
{
    Q_OBJECT
public:
    explicit MyUDPSocket(QUdpSocket *newudpsocket,QObject *parent = nullptr);

    //定义成员变量udpsocket
    QUdpSocket *udpsocket;

signals:

public slots:
    //自定义一个槽函数,完成端口的解除绑定和重新绑定。
    void unbind();
};

#endif // MYUDPSOCKET_H

myudpsocket.cpp

#include "myudpsocket.h"
#include "QDebug"

//第一个参数是构造时传入的newudpsocket
MyUDPSocket::MyUDPSocket(QUdpSocket *newudpsocket,QObject *parent) : QObject(parent)
{
    udpsocket = newudpsocket;
}

void MyUDPSocket::unbind(){
    udpsocket->abort();
    qDebug()<<"Unbind";
    qDebug()<<"Rebind"<<udpsocket->bind(QHostAddress::AnyIPv4,9998);
}

在mythread当中使用自定义的socket类。
mythread.h

#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QThread>
#include <QUdpSocket>
#include <QDebug>
#include "myudpsocket.h"

class MyThread : public QThread
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = nullptr);
    QUdpSocket *udpSocketReceiver;
    MyUDPSocket *myUdpSocket;


signals:

public slots:

    void onSocketReadyRead(); //read data from udpsocket;

protected:
    void run(); // 新线程入口
};

#endif // MYTHREAD_H

mythread.cpp

#include "mythread.h"
#include "QDebug"

MyThread::MyThread(QObject *parent) : QThread(parent)
{

}
void MyThread::onSocketReadyRead(){
    qDebug()<<"Recfunc";
    QByteArray array;
    QHostAddress address;
    quint16 port;
    array.resize(myUdpSocket->udpsocket->bytesAvailable());//根据可读数据来设置空间大小
    myUdpSocket->udpsocket->readDatagram(array.data(),array.size(),&address,&port); //读取数据

    //得到当前收到的数据并转换为字符串输出
    qDebug()<<"Receive data"<<QString(array)<<address<<port;
}

void MyThread::run(){
    //这里一定要到run函数里面构造这两个对象实例。
    udpSocketReceiver = new QUdpSocket();
    myUdpSocket = new MyUDPSocket(udpSocketReceiver);

    qDebug()<<"QUdpSocket的所属线程"<<udpSocketReceiver->thread();
    qDebug()<<"myUdpSocket所属线程"<<myUdpSocket->thread();
    connect(myUdpSocket->udpsocket,SIGNAL(readyRead()),this,SLOT(onSocketReadyRead()),Qt::DirectConnection);

    //设置默认绑定端口
    myUdpSocket->udpsocket->bind(QHostAddress::AnyIPv4,9999);
    exec();
}

UI线程调用,此处按需取用效果更佳。
mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QThread>
#include <mythread.h>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void on_StartThread_clicked();

    void on_pushButton_clicked();

    void on_pushButton_2_clicked();


signals:
    void abortmyudp();


private:
    Ui::MainWindow *ui;

    MyThread *mythread;

};

#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    mythread = new MyThread();
    mythread->start();
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_StartThread_clicked()
{

}

void MainWindow::on_pushButton_clicked()
{
    connect(this,SIGNAL(abortmyudp()),mythread->myUdpSocket,SLOT(unbind()));
    qDebug()<<"clicked";
    emit abortmyudp();
}

void MainWindow::on_pushButton_2_clicked()
{
    mythread->myUdpSocket->udpsocket->abort();
}

在线程启动后可发现QUdpSocket和myUdpSocket所属的线程id相同。

QUdpSocket的所属线程 MyThread(0x1e9f23e8)
myUdpSocket所属线程 MyThread(0x1e9f23e8)

可以利用上述的Python脚本进行测试:

#不需要建立连接
import socket
import time
#创建socket对象中文
#SOCK_DGRAM    udp模式
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
#发送数据-字节
s.sendto("Good luck!".encode(),("192.168.1.109",9999))
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值