Qt多线程编程爬坑笔记

最近在工作中用到了Qt中的多线程,踩了不少坑,故作下笔记,警示后人 - -!


彩虹笼罩下的华农

Overview

使用多线程编程可以最大限度地调用CPU资源,尤其对于多处理器系统。而对于界面开发而言,多线程一个十分重要的作用就是将复杂的运算处理分开执行,以免造成界面的卡顿和冻结,甚至被系统强制关闭。

先来看看官方文档是怎么教我们搞Qt的多线程的。首先,可以建立一个Woker类和一个Controller类,想要放在线程中的后台处理放在Worker类中,再使用QObject::moveToThread()与子线程关联,如下。

class Worker : public QObject {
    Q_OBJECT

public slots:
    void doWork(const QString &parameter) {
        QString result;
        /* ... here is the expensive or blocking operation ... */
        emit resultReady(result);
    }

signals:
    void resultReady(const QString &result);
};

class Controller : public QObject {
    Q_OBJECT
        QThread workerThread; //建立QThread对象
public:
    Controller() {
        Worker *worker = new Worker;
        worker->moveToThread(&workerThread); //令worker和workerThread关联
        connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
        connect(this, &Controller::operate, worker, &Worker::doWork);
        connect(worker, &Worker::resultReady, this, &Controller::handleResults);
        workerThread.start();
    }
    ~Controller() {
        workerThread.quit();
        workerThread.wait();
    }
public slots:
    void handleResults(const QString &);
signals:
    void operate(const QString &);
};

可以看到,通过信号槽机制,可以控制放在了子线程中的worker并互相通信。由于线程中的信号槽连接默认是队列连接方式(稍后会讲到),你可以安全地连接Worker对象与任意的Qobject对象。

Another way to make code run in a separate thread, is to subclass QThread and reimplement run()。另一种方法就是继承QThread并重写run函数,如下

class WorkerThread : public QThread {
    Q_OBJECT
        void run() override {
        QString result;
        /* ... here is the expensive or blocking operation ... */
        emit resultReady(result);
    }
signals:
    void resultReady(const QString &s);
};

void MyObject::startWorkInAThread() {
    WorkerThread *workerThread = new WorkerThread(this);
    connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
    connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
    workerThread->start();
}

详细的内容自己去看吧,具体用哪种方法看个人习惯了。好像乍眼一看也没什么难的~然而

一号坑:子线程中操作UI

Qt创建的子线程中是不能对UI对象进行任何操作的,即QWidget及其派生类对象,这个是我掉的第一个坑。可能是由于考虑到安全性的问题,所以Qt中子线程不能执行任何关于界面的处理,包括消息框的弹出。正确的操作应该是通过信号槽,将一些参数传递给主线程,让主线程(也就是Controller)去处理。

void Controller::handleResults(const int rslt, QString code,
    QString id, QString desc, QString time) 
{
    lockThread->lck->lock_log << "controller handleResults start!\n";
    lockThread->lck->lock_log.flush();
    //(打开时)加密锁有效
    if (rslt == 1) {
        lockThread->lck->lock_log << "controller handleResults 1!\n";
        lockThread->lck->lock_log.flush();
        m_isAuthorized = true;
        QMessageBox::information(NULL, QStringLiteral("授权信息"),
            QStringLiteral("授权成功!!"), QMessageBox::Yes);
    }
    //加密锁失效
    else if (rslt == 2) {
        lockThread->lck->lock_log << "controller handleResults 2!\n";
        lockThread->lck->lock_log.flush();
        m_isAuthorized = false;
        QMessageBox::information(NULL, QStringLiteral("授权信息"),
            QStringLiteral("锁ID:") + id + "\n" +
            QStringLiteral("校验码:") + code, QMessageBox::Yes);
    }
....

个人在VS2017环境下,在子线程中操作UI是直接崩溃的。

二号坑:信号的参数问题

这个就实属有毒,搞了我好久。这个涉及到了Qt的元对象系统(Meta-Object System)和信号槽机制。

元对象系统即是提供了Qt类对象之间的信号槽机制的系统。要使用信号槽机制,类必须继承自QObject类,并在私有声明区域声明Q_OBJECT宏。当一个cpp文件中的类声明带有这个宏,就会有一个叫moc工具的玩意创建另一个以moc开头的cpp源文件(在debug目录下),其中包含了为每一个类生成的元对象代码。


moc开头的cpp文件

在使用connect函数的时候,我们一般会把最后一个参数忽略掉。这时候我们需要看下函数原型

[static] QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, 
    const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)

可以看到,最后一个参数代表的是连接的方式。


Qt::ConnectionType

我们一般会用到方式是有三种

  • 自动连接(AutoConnection),默认的连接方式。如果信号与槽,也就是发送者与接受者在同一线程,等同于直接连接;如果发送者与接受者处在不同线程,等同于队列连接。

  • 直接连接(DirectConnection)。当信号发射时,槽函数立即直接调用。无论槽函数所属对象在哪个线程,槽函数总在发送者所在线程执行。

  • 队列连接(QueuedConnection)。当控制权回到接受者所在线程的事件循环时,槽函数被调用。这时候需要将信号的参数塞到信号队列里。槽函数在接受者所在线程执行。

所以在线程间进行信号槽连接时,使用的是队列连接方式。在项目中,我定义的信号和槽的参数是这样的

signals:
    //自定义发送的信号
    void myThreadSignal(const int, string, string, string, string);

貌似没什么问题,然而实际运行起来槽函数根本就没有被调用,程序没有崩溃,VS也没报错。在查阅了N多博客和资料中才发现,在线程间进行信号槽连接时,参数不能随便写。

为什么呢?我的后四个参数是标准库中的string类型,这不是元对象系统内置的类型,也不是c++的基本类型,系统无法识别,然后就没有进入信号槽队列中了,自然就会出现问题。解决方法有三种,最简单的就是使用Qt的数据类型了

signals:
    //自定义发送的信号
    void myThreadSignal(const int, QString, QString, QString, QString);

第二种方法就是往元对象系统里注册这个类型。注意,在qRegisterMetaType函数被调用时,这个类型应该要确保已经被完好地定义了。

qRegisterMetaType<MyClass>("MyClass");

方法三是改变信号槽的连接方式,将默认的队列连接方式改为直接连接方式,这样的话信号的参数直接进入槽函数中被使用,槽函数立刻调用,不会进入信号槽队列中。但这种方式官方认为有风险,不建议使用。

connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::DirectConnection)

至此,这个坑应该是能爬出来了。

其它需要注意的问题

还有几点需要注意

  • 一定要用信号槽机制,别想着直接调用,你会发现并没有在子线程中执行。

  • 自定义的类不能指定父对象,因为moveToThread函数会将线程对象指定为自定义的类的父对象,当自定义的类对象已经有了父对象,就会报错。

  • 当一个变量需要在多个线程间进行访问时,最好加上voliate关键字,以免读取到的是旧的值。当然,Qt中提供了线程同步的支持,比如互斥锁之类的玩意,使用这些方式来访问变量会更加安全。

C++11标准中直接可以使用标准库中提供的多线程类,但个人觉得还是Qt好用

  • Qt也提供了并发编程的支持,虽然这些玩意一般在服务器编程和高并发场景下用的比较多,界面编程比较少用。

至此总结完毕,希望您爬坑愉快 ( :

Reference:
Qt多线程间信号槽传递非QObject类型对象的参数
QT子线程与主线程的信号槽通信
Qt Creator快速入门_第三版 ——霍亚飞
Qt官方文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,226评论 6 524
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,509评论 3 405
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,523评论 0 370
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,181评论 1 302
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,189评论 6 401
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,642评论 1 316
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,993评论 3 431
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,977评论 0 280
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,527评论 1 326
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,547评论 3 347
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,661评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,250评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,991评论 3 340
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,422评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,571评论 1 277
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,241评论 3 382
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,737评论 2 366