首页 Qt 学习之路 2 Qt 学习之路 2(71):线程简介

Qt 学习之路 2(71):线程简介

30 3.1K

前面我们讨论了有关进程以及进程间通讯的相关问题,现在我们开始讨论线程。事实上,现代的程序中,使用线程的概率应该大于进程。特别是在多核时代,随着 CPU 主频的提升,受制于发热量的限制,CPU 散热问题已经进入瓶颈,另辟蹊径地提高程序运行效率就是使用线程,充分利用多核的优势。有关线程和进程的区别已经超出了本章的范畴,我们简单提一句,一个进程可以有一个或更多线程同时运行。线程可以看做是“轻量级进程”,进程完全由操作系统管理,线程即可以由操作系统管理,也可以由应用程序管理。

Qt 使用QThread 来管理线程。下面来看一个简单的例子:

///!!! Qt5
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    QWidget *widget = new QWidget(this);
    QVBoxLayout *layout = new QVBoxLayout;
    widget->setLayout(layout);
    QLCDNumber *lcdNumber = new QLCDNumber(this);
    layout->addWidget(lcdNumber);
    QPushButton *button = new QPushButton(tr("Start"), this);
    layout->addWidget(button);
    setCentralWidget(widget);

    QTimer *timer = new QTimer(this);
    connect(timer, &QTimer::timeout, [=]() {
        static int sec = 0;
        lcdNumber->display(QString::number(sec++));
    });

    WorkerThread *thread = new WorkerThread(this);
    connect(button, &QPushButton::clicked, [=]() {
        timer->start(1);
        for (int i = 0; i < 2000000000; i++);
        timer->stop();
    });
}

我们的主界面有一个用于显示时间的 LCD 数字面板还有一个用于启动任务的按钮。程序的目的是用户点击按钮,开始一个非常耗时的运算(程序中我们以一个 2000000000 次的循环来替代这个非常耗时的工作,在真实的程序中,这可能是一个网络访问,可能是需要复制一个很大的文件或者其它任务),同时 LCD 开始显示逝去的毫秒数。毫秒数通过一个计时器QTimer进行更新。计算完成后,计时器停止。这是一个很简单的应用,也看不出有任何问题。但是当我们开始运行程序时,问题就来了:点击按钮之后,程序界面直接停止响应,直到循环结束才开始重新更新。

有经验的开发者立即指出,这里需要使用线程。这是因为 Qt 中所有界面都是在 UI 线程中(也被称为主线程,就是执行了QApplication::exec()的线程),在这个线程中执行耗时的操作(比如那个循环),就会阻塞 UI 线程,从而让界面停止响应。界面停止响应,用户体验自然不好,不过更严重的是,有些窗口管理程序会检测到你的程序已经失去响应,可能会建议用户强制停止程序,这样一来你的程序可能就此终止,任务再也无法完成。所以,为了避免这一问题,我们要使用 QThread 开启一个新的线程:

///!!! Qt5
class WorkerThread : public QThread
{
    Q_OBJECT
public:
    WorkerThread(QObject *parent = 0)
        : QThread(parent)
    {
    }
protected:
    void run()
    {
        for (int i = 0; i < 1000000000; i++);
        emit done();
    }
signals:
    void done();
};

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    QWidget *widget = new QWidget(this);
    QVBoxLayout *layout = new QVBoxLayout;
    widget->setLayout(layout);
    lcdNumber = new QLCDNumber(this);
    layout->addWidget(lcdNumber);
    QPushButton *button = new QPushButton(tr("Start"), this);
    layout->addWidget(button);
    setCentralWidget(widget);

    QTimer *timer = new QTimer(this);
    connect(timer, &QTimer::timeout, [=]() {
        static int sec = 0;
        lcdNumber->display(QString::number(sec++));
    });

    WorkerThread *thread = new WorkerThread(this);
    connect(thread, &WorkerThread::done, timer, &QTimer::stop);
    connect(thread, &WorkerThread::finished, thread, &WorkerThread::deleteLater);
    connect(button, &QPushButton::clicked, [=]() {
        timer->start(1);
        thread->start();
    });
}

注意,我们增加了一个WorkerThread类。WorkerThread继承自QThread类,重写了其run()函数。我们可以认为,run()函数就是新的线程需要执行的代码。在这里就是要执行这个循环,然后发出计算完成的信号。而在按钮点击的槽函数中,使用QThread::start()函数启动一个线程(注意,这里不是run()函数)。再次运行程序,你会发现现在界面已经不会被阻塞了。另外,我们将WorkerThread::deleteLater()函数与WorkerThread::finished()信号连接起来,当线程完成时,系统可以帮我们清除线程实例。这里的finished()信号是系统发出的,与我们自定义的done()信号无关。

这是 Qt 线程的最基本的使用方式之一(确切的说,这种使用已经不大推荐使用,不过因为看起来很清晰,而且简单使用起来也没有什么问题,所以还是有必要介绍)。代码看起来很简单,不过,如果你认为 Qt 的多线程编程也很简单,那就大错特错了。Qt 多线程的优势设计使得它使用起来变得容易,但是坑很多,稍不留神就会被绊住,尤其是涉及到与 QObject 交互的情况。稍懂多线程开发的童鞋都会知道,调试多线程开发简直就是煎熬。下面几章,我们会更详细介绍有关多线程编程的相关内容。

30 评论

Sanhu Li 李三乎 2014年3月20日 - 01:08

第二段代码第26行,是不是应该是
QLCDNumber *lcdNumber = new QLCDNumber(this);

回复
豆子 2014年3月20日 - 08:51

因为这个对象声明成成员变量了,在头文件中有声明,所以这里就没有再添加了。

回复
welliam 2014年4月11日 - 16:38

报告错别字,倒数第二段的倒数第二行
“系统可以帮我们清楚线程实例”

回复
豆子 2014年4月12日 - 09:56

多谢指出!

回复
yanluoxichen 2014年7月17日 - 15:45

点击一次按钮之后,再次点击运行一次会报错

回复
ngz 2014年7月29日 - 08:18

呵呵 cpu主频没有进入瓶颈 实际上将cpu主频提升到5-6G毫无难度 是cpu散热进入了瓶颈 于是要用多核技术降低主频以便于散热

回复
豆子 2014年7月29日 - 09:14

感谢指出

回复
yupan 2014年10月21日 - 00:17

计时器的connect函数提示失败怎么回事,需要加什么标识符吗

回复
豆子 2014年10月21日 - 12:57

不知道你说的失败是怎么回事?什么错误?

回复
騡月 2016年7月10日 - 19:18

我也是,提示错误,怎么写都是错的,搞不懂

回复
jason 2014年10月28日 - 12:21

connect 失败加这个: CONFIG += c++11

回复
liu 2015年1月16日 - 13:53

你好,我想问下 你的workthread和Mianwindow 这个两个类是在一个文件中的吗?我把它放在一个文件mainwindow中的话会报错的 undeined reference to vtable for ......,如果把workthread放到一个头文件中的话,然后再mainwindow中包含的话可以正常运行的。

回复
豆子 2015年1月17日 - 12:29

应该是没有关系的。需要注意的是,所有带有Q_OBJECT的类都应该放在头文件中,因为 moc 只会处理头文件中的Q_OBJECT。注意WorkerThread是带有Q_OBJECT的。如果还是想放在 CPP 文件中,需要显式 include 这个 moc 文件,例如增加这样的语句:#include "mainwindow.moc"

回复
2015年1月24日 - 21:39

谢谢,但是我貌似没有找到mainwindow.moc这个文件在哪里?在debug目录下吗
,邮件当做垃圾文件了,不好意思,所以没有及时的回复。

回复
豆子 2015年1月27日 - 09:14

这个文件是 moc 自动生成的,默认会在 debug 相关文件夹下,只需要直接 include 进来就可以了,如果没有特殊设置的话,一般是没有问题的。

回复
2015年1月27日 - 13:53

好的,谢谢。

Shu 2015年5月11日 - 06:01

豆子你好。原文提到”使用QThread::start()函数启动一个线程(注意,这里不是run()函数)”,加qDebug后发现调用的是WorkerThread()解析函数,请问何时调用run()函数呢?另外,如果用上述方法实现多线程,各线程内的run()函数的调用顺序是怎样的呢?是否与各线程调用start()函数的先后顺序一致?

回复
豆子 2015年6月3日 - 11:21

run() 函数是由 start() 函数调用的,我们不需要直接调用 run() 函数。多个线程的 run() 函数调用顺序是由具体线程模型的实现方式决定的,应用程序是不能控制的,除非自己加锁进行控制。

回复
luodanoo 2015年5月22日 - 15:04

connect(timer, &QTimer::timeout, [=]() {
static int sec = 0;
lcdNumber->display(QString::number(sec++));
});这里编译错误。 error: no matching function for call to 'MainWindow::connect(QTimer*&, void (QTimer::*)(QTimer::QPrivateSignal), MainWindow::MainWindow(QWidget*)::)'。请问这是什么原因呢?

回复
豆子 2015年6月3日 - 09:45

一般是环境问题,可能是没有使用支持 C++ 11 的编译器,或者是使用了错误的 Qt 版本

回复
jdlander 2015年5月28日 - 20:45

点击一次按钮之后,再次点击运行一次会报错,我猜想是不是因为thread对象被deletLater()销毁了之后还调用了thread->start()导致sigmentation fault

回复
豆子 2015年6月3日 - 09:39

个人感觉是的

回复
奋斗的蜗牛 2015年7月29日 - 15:03

请问楼主我如果重写Qthread子类,并在其中写入图像处理部分的函数,我现在想要进行批处理,实例化多个Qthread子类,同时进行图像处理,但是我发现图像处理函数不能同时运行,这是为什么?

回复
豆子 2015年8月1日 - 15:05

这个不确定是怎么回事,如果设计正确,允许重入的话是应该没有问题的。

回复
王先先 2015年9月24日 - 15:40

豆子大神:
1.第二段代码的lcdnumber 定义的方式 没有和第一段同步。

2. 5.5 需要在。pro文件中加热 CONFIG += C++ 11 让其支持

3. 第一段代码中的 WorkerThread 类 好像在。h 的头文件里写个继承吧。

回复
sarrow 2016年4月7日 - 21:15

{
...
private:
WorkerThread * thread;
QTimer * timer;
};
...
{
connect(thread, &WorkerThread::finished, [&]() {
thread->deleteLater();
thread = new WorkerThread(this);
connect(thread, &WorkerThread::done, timer, &QTimer::stop);
});
connect(button, &QPushButton::clicked, [&]() {
this->timer->start(1);
this->thread->start();
});

}

回复
jess 2016年7月7日 - 17:27

你好我有个问题想了解一下,qt的线程和槽函数是同一个线程还是槽函数是另外一个线程

回复
豆子 2016年7月8日 - 14:24

Qt 槽函数与线程没有必然联系,槽函数在接收者线程执行

回复
徐晓晓 2017年3月27日 - 19:06

现在还能解决问题吗,楼主?

第二个代码的33行和41行处,中括号“[ ]”报错:expected primary-expression before ‘ [ ’ token.

回复
豆子 2017年3月29日 - 15:01

可能是因为你的编译器不支持 C++11 lambda 表达式。试着在 .pro 文件中添加 CONFIG += c++11,然后重新编译看看行不行;如果不行,只能换编译器。

回复

回复 liu 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2