多线程 UI 交互的问题

QQmlApplicationEngine engine;
FD_UDP* udpWorker = new FD_UDP();
QThread* udpThread = new QThread();
udpWorker->moveToThread(udpThread);
// 当线程启动时,调用udpWorker的receiveDataFormXP()函数
QObject::connect(udpThread, &QThread::started, udpWorker, &FD_UDP::receiveDataFormXP);
udpThread->start();
//expose the positon data to qml
engine.rootContext()->setContextProperty(“udpWorker”,udpWorker);
子线程利用udp接收数据存储起来,在qml中实时使用这些数据,二者有线程上的冲突.

当我试图在ui界面,实时适用子线程中的数据时,如

    Text {
        id: position
        text: (udpWorker.targetDataQML.length > 1)
            ? "Longitude: " + udpWorker.targetDataQML[0] + ", Latitude: " + udpWorker.targetDataQML[1]
            : "Waiting for position data..."
        x: 0
        y: 0
        color: "blue"
    }

会报错如下:
在这里插入图片描述
当前的代码中,将 udpWorker 移动到子线程以处理 UDP 数据接收,但将其暴露给 QML 引擎 (engine.rootContext()->setContextProperty("udpWorker", udpWorker)) 会导致跨线程访问问题,因为 QML 引擎和 UDP worker 位于不同的线程。

这是一个典型的多线程和 UI 交互的问题,QML 和 UI 组件只能在主线程中运行,而当前是直接在子线程中操作数据,并通过 setContextProperty 直接暴露了 udpWorker。这种情况下,如果 QML 试图访问子线程的数据,就会产生线程冲突(正如错误提示)。

解决方案

方法一:

信号与槽方法:

  1. 数据处理逻辑保持在子线程中,UI 更新在主线程:你可以让 udpWorker 在子线程中接收数据,但通过信号与槽机制将数据传递给主线程,再由主线程负责更新 QML。

  2. 使用 QueuedConnection 机制确保线程安全:Qt 的信号槽机制提供了 Qt::QueuedConnection,它可以确保跨线程的信号传递是在接收线程(主线程)处理的。

修改后的代码结构

可以按照如下步骤修改你的代码:

  1. 子线程接收数据,发送信号给主线程:当子线程接收到数据时,发出一个信号,传递数据给主线程处理。
  2. 主线程更新 QML:在主线程中接收信号,然后更新 UI 或 QML 属性。

代码实现

1. 修改 FD_UDP 类,添加信号:
class FD_UDP : public QObject {
    Q_OBJECT
public:
    explicit FD_UDP(QObject *parent = nullptr) : QObject(parent) {}
    
public slots:
    void receiveDataFormXP() {
        // 子线程中接收数据逻辑
        // 假设接收到的经纬度数据是 lat 和 lon
        double lat = ...;  // 接收到的纬度数据
        double lon = ...;  // 接收到的经度数据

        // 发出信号,传递数据到主线程
        emit positionDataReceived(lat, lon);
    }

signals:
    void positionDataReceived(double lat, double lon);  // 信号,将数据传递给主线程
};
2. 在主线程中接收数据并更新 QML

在主线程中连接信号并更新数据:

// 创建 UDP worker 和线程
FD_UDP* udpWorker = new FD_UDP();
QThread* udpThread = new QThread();
udpWorker->moveToThread(udpThread);

// 当线程启动时,调用 udpWorker 的 receiveDataFormXP() 函数
QObject::connect(udpThread, &QThread::started, udpWorker, &FD_UDP::receiveDataFormXP);

// 当接收到数据时,将数据传递给主线程
QObject::connect(udpWorker, &FD_UDP::positionDataReceived, [&](double lat, double lon) {
    // 通过 QML 引擎更新经纬度数据
    engine.rootContext()->setContextProperty("longitude", lat);
    engine.rootContext()->setContextProperty("latitude", lon);
}, Qt::QueuedConnection);

// 启动线程
udpThread->start();
3. 在 QML 中使用 latitudelongitude

你可以在 QML 中使用暴露的 latitudelongitude 数据进行显示:

Text {
    id: position
    text: "Longitude: " + longitude + ", Latitude: " + latitude
    x: 0
    y: 0
    color: "blue"
}

说明:

  • 信号与槽机制:这里使用了 QObject::connect,将 FD_UDP::positionDataReceived 信号连接到主线程中的一个 lambda 函数。在该函数中,我们通过 setContextPropertylatitudelongitude 更新到 QML 上。
  • Qt::QueuedConnection:我们指定了 Qt::QueuedConnection,确保槽函数在主线程的事件循环中执行,从而避免跨线程访问引发的错误。
  • 更新 QML 属性:每次接收到新的经纬度数据时,都会更新 QML 中的 longitudelatitude,确保数据是实时显示的。

方法二

使用线程安全的数据结构

如果你需要在主线程和子线程之间共享数据,可以使用线程安全的数据结构来存储和访问数据。例如,使用 QMutex 来保护数据访问:
注意要将qml使用的数据使用property注册,并且创建相应的getter和writer。

  1. 定义线程安全的数据结构

    class SharedData : public QObject {
        Q_OBJECT
    public:
        explicit SharedData(QObject* parent = nullptr) : QObject(parent) {}
    
        double longitude;
        double latitude;
        double true_airspeed;
        double heading;
    
        QMutex mutex;  // 互斥锁
    };
    
  2. 在子线程中更新数据

    void FD_UDP::receiveDataFromXP() {
        while (running) {
            for (int attempt = 0; attempt <= RETRY_ATTEMPTS; ++attempt) {
                int result = getDREFs(sock, drefs, results, 4, sizes);
                if (result >= 0) {
                    double lon = results[0][0];
                    double lat = results[1][0];
                    double true_airspeed = results[2][0];
                    double heading = results[3][0];
    
                    QMutexLocker locker(&sharedData->mutex);  // 锁定互斥锁
                    sharedData->longitude = lon;
                    sharedData->latitude = lat;
                    sharedData->true_airspeed = true_airspeed;
                    sharedData->heading = heading;
                    locker.unlock();
                    break;  // 退出重试循环
                }
            }
        }
    }
    
  3. 在主线程中访问数据

    // 在主线程中访问共享数据
    QMutexLocker locker(&sharedData->mutex);  // 锁定互斥锁
    double lon = sharedData->longitude;
    double lat = sharedData->latitude;
    double true_airspeed = sharedData->true_airspeed;
    double heading = sharedData->heading;
    locker.unlock();
    

sharedData初始化

sharedData 需要在主线程中初始化,并在子线程中访问。你可以在主线程中创建一个 SharedData 实例,并将其传递给子线程,以便子线程能够访问和更新共享数据。以下是如何初始化 sharedData 并在主线程和子线程之间进行传递的详细步骤:

1. 定义 SharedData

首先,定义 SharedData 类,该类包含需要共享的数据和一个互斥锁以确保线程安全。

class SharedData : public QObject {
    Q_OBJECT
public:
    explicit SharedData(QObject* parent = nullptr) : QObject(parent) {}

    double longitude;
    double latitude;
    double true_airspeed;
    double heading;

    QMutex mutex;  // 互斥锁,保护数据
};

2. 在主线程中初始化 SharedData

在主线程中创建一个 SharedData 实例,并将其传递给子线程中的工作对象。

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    // 创建共享数据实例
    SharedData* sharedData = new SharedData();

    // 创建工作对象和线程
    FD_UDP* udpWorker = new FD_UDP();
    QThread* udpThread = new QThread();

    // 将工作对象移动到子线程
    udpWorker->moveToThread(udpThread);

    // 将共享数据传递给工作对象
    udpWorker->setSharedData(sharedData);

    // 连接信号和槽
    QObject::connect(udpThread, &QThread::started, udpWorker, &FD_UDP::receiveDataFromXP);
    udpThread->start();

    // 在 QML 引擎中设置共享数据
    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("sharedData", sharedData);

    return app.exec();
}

3. FD_UDP 中使用 SharedData

FD_UDP 类中,你需要添加一个方法来设置 SharedData 的指针,并在子线程中使用它。

class FD_UDP : public QObject {
    Q_OBJECT
public:
    explicit FD_UDP(QObject* parent = nullptr) : QObject(parent), sharedData(nullptr) {}

    void setSharedData(SharedData* data) {
        sharedData = data;
    }

public slots:
    void receiveDataFromXP() {
        while (running) {
            for (int attempt = 0; attempt <= RETRY_ATTEMPTS; ++attempt) {
                int result = getDREFs(sock, drefs, results, 4, sizes);
                if (result >= 0) {
                    double lon = results[0][0];
                    double lat = results[1][0];
                    double true_airspeed = results[2][0];
                    double heading = results[3][0];

                    // 更新共享数据
                    if (sharedData) {
                        QMutexLocker locker(&sharedData->mutex);
                        sharedData->longitude = lon;
                        sharedData->latitude = lat;
                        sharedData->true_airspeed = true_airspeed;
                        sharedData->heading = heading;
                    }

                    break;  // 退出重试循环
                }
            }
        }
    }

private:
    SharedData* sharedData;
};

4. 在 QML 中使用 SharedData

在 QML 文件中,你可以直接访问 SharedData 对象中的属性。

import QtQuick 2.15
import QtQuick.Controls 2.15

Rectangle {
    width: 640
    height: 480

    Text {
        id: lonText
        text: "Longitude: " + sharedData.longitude
        anchors.centerIn: parent
    }

    Text {
        id: latText
        text: "Latitude: " + sharedData.latitude
        anchors.top: lonText.bottom
        anchors.horizontalCenter: parent.horizontalCenter
    }

    // 其他 UI 元素
}
  1. 在主线程中:初始化 SharedData 实例,并将其传递给子线程中的工作对象。
  2. 在子线程中:使用 SharedData 实例来更新数据。
  3. 在主线程中:将 SharedData 实例传递到 QML 中,并在 QML 文件中使用它来显示数据。

通过这种方式,你可以确保 SharedData 在主线程和子线程之间安全地共享和更新数据。

总结

这样做可以确保子线程负责数据接收,主线程负责 UI 更新,从而避免线程冲突的错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值