Qt GUI程序中单线程和多线程的区别
Qt应用程序exec后就会生成一个线程,这个线程就是主线程,在GUI程序中也称为GUI线程。主线程也是唯一允许创建QApplication或QCoreAppliation对象,比并且可以对创建的对象调用exec()的线程,从而进入事件循环。
在只有主线程即单线程的情况中,每一个事件的发生都需要进入事件循环进行等待,如有在某一步计算量比较大,则会一直占用CPU不放,导致其它操作无法完成,界面陷入冻结状态,例如像下面的情况:
在主界面中有两个按钮,其中一个按钮用于一个非常大的计算并在结束的显示结果,另外一个按钮用于显示文本“hello, world!”。代码如下:
mainwindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void on_calculationButton_clicked();
void on_displayButton_clicked();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
mainwindow.cpp:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QString>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_calculationButton_clicked() //计算
{
int result = 0;
for(int i = 0; i < 2000000000; i++)
result += i;
ui->displayEdit->setText(QString::number(result));
}
void MainWindow::on_displayButton_clicked() //单击显示文本
{
static int flag = 0;
if(flag == 0)
{
flag = 1;
ui->displayEdit->setText(tr("hello, world!"));
}
else
{
flag = 0;
ui->displayEdit->setText(tr("I Love You!"));
}
}
结果:
可以看到,在点击计算按钮之后,进入计算,界面被冻结住了,如果在点击窗口上的其它按钮,则无法反映。
对于这种超大计算量的操作,我们将其分离出去单独处理,处理好了在跟主界面反馈一个结果即可,像这种情况就需要为主线程另外开辟一个新的线程来处理计算,并且在计算结束的时候与主线程通信,反馈结果。反馈结果需要一个同步操作,而Qt中的signal/slot正好能够满足这个要求,于是,我们将计算操作放到另外一个线程中进行,然后将计算结果通过一个signal传递到主线程中的一个slot,从而实现了与主线程之间的通信。代码修改如下:
mainwindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
class Thread;
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void on_calculationButton_clicked(); //计算按钮
void on_displayButton_clicked(); //显式按钮
void displayResult(int);
private:
Ui::MainWindow *ui;
Thread *thread;
};
#endif // MAINWINDOW_H
thread.h:
#ifndef THREAD_H
#define THREAD_H
#include <QThread>
class Thread : public QThread{
Q_OBJECT
public:
Thread(); //构造函数
signals:
void returnResult(int);
protected:
void run();
};
#endif // THREAD_H
mainwindow.cpp:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QString>
#include "thread.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
thread = new Thread();
connect(thread, SIGNAL(returnResult(int)), this, SLOT(displayResult(int)));
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_calculationButton_clicked() //计算
{
thread->start();
}
void MainWindow::on_displayButton_clicked() //单击显示文本
{
static int flag = 0;
if(flag == 0)
{
flag = 1;
ui->displayEdit->setText(tr("hello, world!"));
}
else
{
flag = 0;
ui->displayEdit->setText(tr("I Love You!"));
}
}
void MainWindow::displayResult(int result)
{
ui->displayEdit->setText(QString::number(result));
}
thread.cpp:
#include "thread.h"
Thread::Thread()
{
//
}
void Thread::run()
{
int result = 0;
for(int i = 0; i < 2000000000; i++)
result += i;
emit returnResult(result); //将结果放在信号中
}
运行结果如下:
可以看到,在点击计算按钮之后,在点击显示文本按钮仍然可以得到结果。在另外一个线程中计算结束后,便会改变主线程中的显示结果。
总结:当主线程需要另外一个线程提供服务,需要另外一个线程中的计算结果来修改主线程中的某个控件状态或内容。我们可以为这些超级复杂的任务开辟一个新的线程,在这个线程中执行任务,然后将执行结果通过一个signal来传递给主线程,如果有输出则可以通过signal的参数传递,如果仅仅只需要一个信号则不需要参数。
/************************************************************
1. 引言
多线程对于需要处理耗时任务的应用很有用,一方面响应用户操作、更新界面显示,另一方面在“后台”进行耗时操作,比如大量运算、复制大文件、网络传输等。
使用Qt框架开发应用程序时,使用QThread类可以方便快捷地创建管理多线程。而多线程之间的通信也可使用Qt特有的“信号-槽”机制实现。
下面的说明以文件复制为例。主线程负责提供交互界面,显示复制进度等;子线程负责复制文件。最后附有可以执行的代码。
2. QThread使用方法1——重写run()函数
第一种使用方法是自己写一个类继承QThread,并重写其
run()
函数。
大家知道,C/C++程序都是从
main()
函数开始执行的。
main()
函数其实就是主进程的入口,
main()
函数退出了,则主进程退出,整个进程也就结束了。
而对于使用Qthread创建的进程而言,
run()
函数则是新线程的入口,
run()
函数退出,意味着线程的终止。复制文件的功能,就是在run()函数中执行的。
下面举个文件复制的例子。自定义一个类,继承自Qthread
CopyFileThread: public QThread
{
Q_OBJECT
public:
CopyFileThread(QObject * parent = 0);
protected:
void run(); // 新线程入口
// 省略掉一些内容
}
在对应的cpp文件中,定义run()
void CopyFileThread::run()
{
// 新线程入口
// 初始化和操作放在这里
}
将这个类写好之后,在主线程的代码中生成一个CopyFileThread的实例,例如在mainwindow.cpp中写:
// mainwindow.h中
CopyFileThread * m_cpyThread;
// mainwindow.cpp中
m_cpyThread = new CopyFileThread;
在要开始复制的时候,比如按下“复制”按钮后,让这个线程开始执行:
m_cpyThread->start();
注意,使用start()函数来启动子线程,而不是run()。start()会自动调用run()。
线程开始执行后,就进入run()函数,执行复制文件的操作。而此时,主线程的显示和操作都不受影响。
如果需要进行对复制过程中可能发生的事件进行处理,例如界面显示复制进度、出错返回等等,应该从CopyFileThread中发出信号(signal),并事先连接到mainwindow的槽,由这些槽函数来处理事件。
3. QThread使用方法2——moveToThread()
如果不想每执行一种任务就自定义一个新线程,那么可以自定义用于完成任务的类,并让它们继承自QObject。例如,自定义一个FileCopier类,用于复制文件。
class FileCopier : public QObject
{
Q_OBJECT
public:
explicit FileCopier(QObject *parent = 0);
public slots:
void startCopying();
void cancelCopying();
}
注意这里我们定义了两个槽函数,分别用于复制的开始和取消。
这个类本身的实例化是在主线程中进行的,例如:
// mainwindow.h中
private:
FileCopier* m_copier;
// mainwindow.cpp中,初始化时
m_copier = new FileCopier;
此时m_copier还是属于主线程的。要将其移动到子线程处理,需要首先声明并实例化一个QThread:
// mainwindow.h中
signals:
void startCopyRsquested();
private:
QThread * m_childThread; // m_copier将被移动到此线程执行
// mainwindow.cpp中,初始化时
m_childThread = new QThread; // 子线程,本身不负责复制
然后使用moveToThread()将m_copier移动到新线程。注意moveToThread()是QObject的公有函数,因此用于复制文件的类FileCopier必须继承自QObject。移动之后启动子线程。此时复制还没有开始。
m_copier->moveToThread(m_childThread); // 将实例移动到新的线程,实现多线程运行
m_childThread->start(); // 启动子线程
注意一定要记得启动子线程,否则线程没有运行,m_copier的功能也无法执行。
要开始复制,需要使用信号-槽机制,触发FileCopier的槽函数实现。因此要事先定义信号并连接:
// mainwindow.h中
signals:
void startCopyRsquested();
// mainwindow.cpp中,初始化时
// 使用信号-槽机制,发出开始指令
connect(this, SIGNAL(startCopyRsquested()), m_copier, SLOT(startCopying()));
当按下“复制”按钮后,发出信号。
emit startCopyRsquested(); // 发送信号
m_copier在另一个线程接收到信号后,触发槽函数,开始复制文件。
4.常见问题
4.1. 子线程中能不能进行UI操作?
Qt中的UI操作,比如QMainWindow、QWidget之类的创建、操作,只能位于主线程!
这个限制意味着你不能在新的线程中使用QDialog、QMessageBox等。比如在新线程中复制文件出错,想弹出对话框警告?可以,但是必须将错误信息传到主线程,由主线程实现对话框警告。
因此一般思路是,主线程负责提供界面,子线程负责无UI的单一任务,通过“信号-槽”与主线程交互。
4.2. QThread中的哪些代码属于子线程?
QThread,以及继承QThread的类(以下统称QThread),他们的实例都属于新线程吗?答案是:不。
需要注意的是,QThread本身的实例是属于创建该实例的线程的。比如在主线程中创建一个QThread,那么这个QThread实例本身属于主线程。当然,QThread会开辟一个新线程(入口是
run()
),但是QThread本身并不属于这个新线程。也就是说,QThread本身的成员都不属于新线程,而且在QThread构造函数里通过
new
得到的实例,也不属于新线程。这一特性意味着,如果要实现多线程操作,那么你希望属于新线程的实例、变量等,应该在
run()
中进行初始化、实例化等操作。本文给出的例子就是这样操作的。
如果你的多线程程序运行起来,会出现关于thread的报警,思考一下,各种变量、实例是不是放对了位置,是不是真的位于新的线程里。
4.3. 怎么查看是不是真的实现了多线程?
可以打印出当前线程。对于所有继承自QObject的类,例如QMainwindow、QThread,以及自定义的各种类,可以调用
QObject::thread()
查看当前线程,这个函数返回的是一个QThread的指针。例如用qDebug()打印:
在mainwindow.cpp的某个函数里、QThread的run()函数里、自定义类的某个函数里,写上:
qDebug() << "Current thread:" << thread();
对比不同位置打印的指针,就可以知道它们是不是位于同一个线程了。
5.范例
范例实现了多线程复制文本文件。
提供的范例文件可用QtCreator编译运行。界面如下(不同的操作系统略有不同):
范例中实现了本文介绍的两种方法,同时也给出了单线程复制对比。打钩选择不同的复制方法。可以发现,在使用多线程的时候,界面不会假死,第二根进度条的动画是持续的;而使用单线程复制的时候,“取消”按钮按不动,界面假死,而且第二根进度条的动画也停止了。
由于范例处理的文件很小,为了让复制过程持续较长时间以便使得现象明显,复制文件的时候,每复制一行加入了等待。
/**************************************************************
qt写的GUI程序是多线程的吗?
不是,没有GUI是多线程的。这是个梦,而且是虚妄的。。。它只会带来更多麻烦,所以很多先辈大佬们在幻想并尝试后就放弃了。如果你去翻一些古早文章的话,或许可以找到一些蛛丝马迹。
只是说,因为并非所有GUI控件类的接口都是跟GUI操作紧密相关的,所以也存在一些侥幸情况是你访问了接口但是没有崩。但部分情况下跨线程访问GUI是当场去世的。(一些脚本或者高级封装后的工具(不限于C++),可能会让API Caller产生自己可以跨线程直接操作GUI的错觉,有可能是做了兼容,也有可能是经过了解释层或者侥幸没有触碰到很紧要的部分)
GUI本身始终是在main thread的,因为从常规角度考虑的话,这个需求可能推到最后是矛盾的。一方面的话,你从GUI控件进行交互操作,它逐级响应事件并馈送回来,假如他是异步的,那么就需要逐级加锁解锁,否则状态会出现很多不确定的中间态;二一个方面,他正在异步处理你前面的操作却还没有馈送回来,你除了等待也别无他事可做,所以最终结果的反馈还是“解铃还须系铃人”了。
至于有些人可能会认为,“那如果点了之后,有复杂的数据处理,我多线程至少不会把界面卡死啊”,这个纯粹是程序设计问题,跟有没有多线程GUI没有直接关系。你把复杂数据处理写在一条流水线上,那你挨个点一通,就算是多线程GUI,那多线程也全卡死了。
/*********************************************************************************
Qt中UI线程与子线程的交互
GUI框架一般只允许UI线程操作界面组件,Qt也是如此。但我们的应用程序一般是多线程的,势必就涉及到UI线程与子线程的交互。
下面介绍常用的UI线程与子线程交互方式,并附上自己的Demo。
1.Qt中几种常见的多线程交互的操作
Qt中提供了一些线程安全的方法来让我们使用:
A.使用信号槽
Qt的信号槽是线程安全的。connect函数的第五个参数ConnectionType默认为Qt::AutoConnection,如果接收者和发送者不在一个线程,则相当于自动使用Qt::QueuedConnection类型,槽函数会在接收者线程执行。
connect(this,&MainWindow::signalDoing,worker,&MyWorker::slotDoing);
B.使用 QMetaObject::invokeMethod
invokeMethod可以线程安全的对目标对象进行操作,如调用目标对象的成员函数等。它也具有一个ConnectionType参数,参照connect。
qDebug()<<"main thread"<<QThread::currentThread();
connect(ui->btnDoB,&QPushButton::clicked,this,[this]{
QtConcurrent::run([=]{
qDebug()<<"run doing"<<QThread::currentThread();
QMetaObject::invokeMethod(this,[=]{ //这个this就是传递进来的mainwindow
qDebug()<<"invoke doing"<<QThread::currentThread();
ui->textEditB->append("invoke test");
});
});
});
C.使用 QApplication::postEvent
自定义事件我没怎么用,但postEvent也是线程安全的操作。
QApplication::postEvent(target,new Event(type)));
当然,要完成多线程中与UI的交互不止上面三种方式,可以参照百度其他人提供的思路。
2.示例代码
运行效果:
代码git链接:https://github.com/gongjianbo/MyTestCode/tree/master/Qt/UiAndSubThread
实现代码长了点,贴主要部分:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDateTime>
#include <QtConcurrentRun>
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
setWindowTitle("");
//【示例A】通过信号槽
thread=new QThread(this);
//要move到别的线程的话不要传递parent参数
worker=new MyWorker();
//更改该对象及其子对象的线程关联性。如果对象具有父对象,则无法移动。
//事件处理将在targetThread中继续。
worker->moveToThread(thread);
//官方示例里的释放方式
connect(thread,&QThread::finished,worker,&QObject::deleteLater);
//worker的定时器开关
ui->btnTimer->setCheckable(true); //Checkable就能有两种状态-对应定时器开关
connect(ui->btnTimer,&QPushButton::clicked,worker,&MyWorker::slotTimerSwitch);
//worker执行任务
connect(ui->btnDoA,&QPushButton::clicked,this,[this]{
emit signalDoing(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss:zzz")+" main");
});
connect(this,&MainWindow::signalDoing,worker,&MyWorker::slotDoing);
//worker操作结果
connect(worker,&MyWorker::signalMessage,ui->textEditA,&QTextEdit::append);
//启动线程
thread->start();
//【示例B】通过invokeMethod方法
//(这里我直接用concurrent模块的run函数)
qDebug()<<"main thread"<<QThread::currentThread();
connect(ui->btnDoB,&QPushButton::clicked,this,[this]{
QtConcurrent::run([=]{
qDebug()<<"run doing"<<QThread::currentThread();
//使用QMetaObject::invokeMethod()操作是线程安全的
QMetaObject::invokeMethod(this,[=]{ //这个this就是传递进来的mainwindow
qDebug()<<"invoke doing"<<QThread::currentThread();
ui->textEditB->append(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss:zzz")+" invoke finished");
});
});
});
//【示例C】通过postEvent
//(需要重写接收者的event()事件处理函数)
connect(ui->btnDoC,&QPushButton::clicked,this,[this]{
QtConcurrent::run([=]{
qDebug()<<"run doing"<<QThread::currentThread();
//postEvent是非阻塞的,sendEvent是阻塞的,postEvent是线程安全的
QApplication::postEvent(this,new MyEvent(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss:zzz")));
});
});
}
MainWindow::~MainWindow()
{
//退出示例A的线程,注意里面若有死循环要提前break
thread->quit();
thread->wait();
delete ui;
}
bool MainWindow::event(QEvent *event)
{
if(event->type()==MyEvent::eventType){
MyEvent *my_event=dynamic_cast<MyEvent*>(event);
if(my_event){
//通过postevent传递信息
ui->textEditB->append(my_event->message+" event finished");
}
}
return QMainWindow::event(event);
}
/*****************************************************************
在进行桌面应用程序开发的时候,假设应用程序在某些情况下需要处理比较复杂的逻辑,如果只有一个线程去处理,就会导致窗口卡顿,无法处理用户的相关操作。这种情况下就需要使用多线程,其中一个线程处理窗口事件,其他线程进行逻辑运算,多个线程各司其职,不仅可以提高用户体验还可以提升程序的执行效率。
在qt中使用了多线程,有些事项是需要额外注意的:
1.默认的线程在Qt中称之为窗口线程,也叫主线程,负责窗口事件处理或者窗口控件数据的更新。
2.子线程负责后台的业务逻辑处理,子线程中不能对窗口对象做任何操作,这些事情需要交给窗口线程处理。
3.主线程和子线程之间如果要进行数据的传递,需要使用Qt中的信号槽机制。
1.多线程的理解
在操作系统中线程和进程划分。
操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。
线程之间相互独立,抢占式执行。对于单核CPU来说同一时刻只能有一个线程执行,一个进程执行。
但是由于CPU不断在这些进程间轮换执行,速度相对人的反应很快,不容易察觉。
既然这样,为什么要使用多线程呢?
a.对于多核cpu,多线程程序充分利用硬件优势
b.对于单核cpu,由于线程上下文的切换会降低整体运行效率。但是为了防止执行耗时操作时界面假死,我们必须把耗时操作单独放在线程中后台执行,防止阻塞主线程无法刷新窗口。
我们来看一下程序的阻塞和非阻塞
这里主要说对于线程之间的公共资源,同时只能由一个线程操作,在此期间其他线程访问将会被挂起直到上一次访问结束,同样客户端执行界面刷新的主线程也会挂起。
非阻塞指的是,一个线程的操作不会阻塞其他线程对事件的接受和处理。
同步和异步
这里说的执行一个操作必须等待执行完成,下面的逻辑才会继续执行,是为同步执行代码
对函数调用后,不会等待执行结果,继续执行下面的代码,是为异步执行。
2.线程使用
QT中多线程得两种处理方法
使用线程在程序执行长时间操作得时候弹出进度条
使用线程可以把占据时间长得程序中任务放到后台去处理
其中一种是继承QThread得run函数,另外一种是把一个继承于QObject得类转移到一个Thread里。
1.继承QThread
QThread继承类只有run函数是在新线程里跑的,其他函数在创建QThread线程中运行
新建一个线程类ExportThread:QThread ,把耗时操作放在其中run函数中
2.把一个继承于QObject的类转移到一个Thread里
创建一个继承自QObject类得类对象object,使用object.moveToThread(QThread *);
3.线程类中得槽函数在哪个线程中执行得问题
对于方法1中,槽函数在创建线程类对象得线程(一般是主线程)中执行
对于方法2中,槽函数在次线程中执行,通过信号槽调用,直接调用则都在调用线程中执行,
所以要把耗时操作放在槽函数中,外面信号触发,
具体需要参考,connect函数中表示连接方式得参数
如下:
同步调用:信号发出后,当前线程等待槽函数执行完毕才能执行剩余代码。
异步调用:信号发出后,立即执行剩余逻辑,不关心槽函数什么时候执行。
AutoConnection 信号和槽同一线程时,直接联,不同线程时,队列联
DirectConnection 直接联,在主线程中执行,同步调用,不依赖QT事件循环
QueueConnection 队列,次线程中执行,异步调用,槽函数所在对象得线程必须启用QT事件循环
BlockingQueuedConnection 阻塞联,同步调用, 槽函数在次线程中执行,用信号量实现阻塞,
槽函数所在对象得线程必须启用QT事件循环,此连接只能用于发
出信号得线程和槽函数执行线程不同得情况。
要么在发射信号得线程中执行
要么在接受者依附得线程中执行
线程安全
/**************************************************************
线程与进程有什么区别呢?
举个简单的例子,当你在LOL的时候,在相同的桌面上可能有一个播放器正在播放你最喜欢的歌曲。这是一个两个进程并行工作的例子:一个进程运行LOL程序;另一个进程运行一个媒体播放器。这种情况最适合用多任务这个词来描述。进一步观察播放器,你会发现在这个进程内,又存在并行的工作。当播放器向音频驱动发送音乐数据的时候,用户界面上与之相关的歌词信息不断地进行更新。这就是单个进程内的并行线程。应用程序可以使用多线程技术来提高程序性能,改善用户体验。尤其是在多核处理器中,多个线程可以在多个CPU上同时运行,能够有效提高程序的运行速度。如果一个应用程序需要处理一些耗时的数据处理计算,应该用一个独立的线程来做这些运算,而用另外一个应用程序来处理和用户的交互。这样,用户可以提前中断或者暂停数据处理工作,或者可以做些和数据计算无关的工作,提高用户的工作效率。
那么,线程的并行性是如何实现的呢?在单核CPU计算机上,并行工作类似在电影院中不停移动图像产生的一种假象。对于进程而言,在很短的时间内中断占有处理器的进程就形成了这种假象。然而,处理器迁移到下一个进程。为了在不同进程之间进行切换,当前程序计算器被保存,下一个程序计算器被加载进来。这还不够,相关寄存器以及一些体系结构和操作系统特定的数据也要进行保存和重新加载。
就像一个CPU可以支撑两个或多个进程一样,同样也可以让CPU在单个进程内运行不同的代码片段。当一个进程启动时,它问题执行一个代码片断从而该进程就被认为是拥有了一个线程。但是,该程序可以会决定启动第二个线程。这样,在一个进程内部,两个不同的代码序列就需要被同步处理。通过不停地保存当前线程的程序计数器和相关寄存器,同时加载下一个线程的程序计数器和相关寄存器,就可以在单核CPU上实现并行。在不同活跃线程之间的切换不需要这些线程之间的任何协作。当切换到下一个线程时,当前线程可能处于任一种状态。
当前CPU设计的趋势是拥有多个核。一个典型的单线程应用程序只能利用一个核。但是,一个多线程程序可被分配给多个核,便得程序以一种完全并行的方式运行。这样,将一个任务分配给多个线程使得程序在多核CPU计算机上的运行速度比传统的单核CPU计算机上的运行速度快很多。何为线程 、GUI线程和工作者线程 ?数据的同步访问何时使用线程,何时不应使用线程?该使用哪种Qt线程技术? Qt 线程基础 QObject和Threads 保护数据的完整性!处理异步执行示例和 深入研究更多参考GUI线程和工作者线程GUI线程和工作者线程。
如上所述,每个程序启动后就会拥有一个线程。该线程称为”主线程”(在Qt应用程序中也叫”GUI线程”)。Qt GUI必须运行在此线程上。所有的图形元件和几个相关的类,如QPixmap,不能工作于非主线程中。非主线程通常称为”工作者线程”,因为它主要处理从主线程中卸下的一些工作。如数据的同步访问。
每个线程都有自己的栈,这意味着每个线程都拥有自己的调用历史和本地变量。不同于进程,同一进程下的线程之间共享相同的地址空间。下图1-1显示了内存中的线程块图。非活跃线程的程序计数器和相关寄存器通常保存在内核空间中。对每个线程来说,存在一个共享的代码片段和一个单独的栈。
图 1-1
如果两个线程拥有一个指向相同对象的指针,那么两个线程可以同时去访问该对象,这可以破坏该对象的完整性。很容易想象的情形是一个对象的两个方法同时执行可能会出错。
有时,从不同线程中访问一个对象是不可避免的。例如,当位于不同线程中的许多对象之间需要进行通信时。由于线程之间使用相同的地址空间,线程之间进行数据交换要比进程之间进行数据交换快得多。数据不需要序列化然后拷贝。线程之间传递指针是允许的,但是必须严格协调哪些线程使用哪些指针。禁止在同一对象上执行同步操作。有一些方法可以实现这种要求,下面描述其中的一些方法。
那么,怎样做才安全呢?在一个线程中创建的所有对象在线程内部使用是安全的,前提条件是其他线程没有引用该线程中创建的一些对象且这些对象与其他的线程之间没有隐性耦合关系。当数据作为静态成员变量,单例或全局数据方式共享时,这种隐性耦合是可能发生的。
使用线程基本上,对线程来讲,有两种使用情形:
将一些处理时间较长或阻塞的任务移交给其他的线程,从而保证GUI线程或其他对时间敏感的线程保持良好的反应速度。何时不应使用线程。开发者在使用线程时必须特意小心。启动其他线程很容易,但很难保证所有共享的数据仍然是一致的。这些问题通常很难找到,因为它们可以在某个时候仅显示一次或仅在某种硬件配置下出现。在创建线程解决某些问题之前,如下的一些方法也应该考虑一下。
一般来讲,建议只使用安全的且已被验证过的路径,避免引入线程概念。 QtConcurrent提供了一种简易的接口,来将工作分配到所有的处理器的核上。线程相关代码已经完全隐藏在QtConcurrent 框架中,因此,开发者不需要关注这些细节。但是, QtConcurrent 不能用于那么需要与运行中的线程进行通信的情形,且它也不能用于处理阻塞操作。
该使用哪种 Qt 线程技术?
有时,我们不仅仅只是在另一个线程中运行一个方法。可能需要位于其他线程中的某个对象为GUI线程提供服务。也许,你想其他的线程一直保持活跃状态去不停地轮询硬件端口并在一个需要关注的事件发生时发送一个信号给GUI线程。Qt提供了不同的解决方案来开发多线程应用程序。正确的解决方案取决于新线程的目的以及它的生命周期。
QThread 是对本地平台线程的一个非常好的跨平台抽象。启动一个线程非常简单。让我们看一段代码,它产生另一个线程,该线程打印data,然后退出。
// Userthread.h
class UserThread : public QThread
{
Q_OBJECT
private:
void run();
};
我们从QThread 中派生一个类并重载run()方法。
// Userthread.cpp
void UserThread::run()
{
qDebug() <<"data from worker thread " << thread()->currentThreadId();
}
run方法中包含的代码会运行于一个单独的线程。在本例中,一条包含线程ID的信号将会被输出来。QThread::start() 会在另一个线程中调用该方法。
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Userthread thread;
thread.start();
qDebug() <<"data from GUI thread " <<app.thread()->currentThreadId();
thread.wait();
return 0;
}
为了启动该线程,我们的线程对象必须被初始化。start() 方法创建了一个新的线程并在新线程中调用重载的run() 方法。在 start() 被调用后,有两个程序计数器走过程序代码。主函数启动,且仅有一个GUI线程运行,它停止时也只有一个GUI线程运行。当另一个线程仍然忙碌时退出程序是一种编程错误,因此, wait方法被调用用来阻塞调用的线程直到run()方法执行完毕。
下面是运行代码的结果:
data from worker thread 0x3b4c
data from GUI thread 0x3598
QObject 和线程:
一个 QObject 通常被认为有线程亲和力 或换句话说, 它位于某个线程中。这意味着,在创建的时候, QObject保存了一个指向当前线程的指针。当一个事件利用 postEvent()发出时,该信息就变得有关了。该事件将会被放于对应线程的事件循环中。如果QObject位于的线程没有事件循环,那么事件就不会被传递。
为了启动一个事件循环,exec()必须在 run()里面调用. 线程亲和力可使用moveToThread()来改变。如上所述,开发者从其他线程中调用对象的方法时必须非常小心。线程亲和力并没有改变这种状况。Qt文档标记了几个方法是线程安全的。 postEvent() 是一个很明显的例子。一个线程安全的方法可以在不同的线程中同时被调用。
在没有并行访问方法的情况下,在其他线程中调用对象的非线程安全的方法时可能运行了几千次后才会出现一个并发访问,造成不可预料的行为。编写测试代码并不能完全的保证线程的正确性,但仍然很重要。在Linux中,Valgrind和Helgrind可以侦测线程错误。
QThread 细节非常有意思:
QThread 并不位于新线程 run()执行的位置中。它位于旧线程中。
大部分QThread 的方法是线程的控制接口中,并在旧线程中调用。不要使用moveToThread()将这些接口移到新创建的线程中,例如,调用moveToThread(this) 被认为是一种坏的实践。
exec()和静态方法usleep(),msleep(), sleep()应在新创建的线程中调用。
其他的一些定义在QThread 子类中的成员可以在新旧线程中访问。开发者负责协调这些访问。 一种典型的策略是在调用start() 前设置这些成员。一旦工作者线程运行起来,主线程不应当再修改这些成员。当工作者线程停止后,主线程又可以访问些额外的成员。这是一种在线程启动前和停止后传递参数的方便的策略。
一个 QObject’s 父类必须位于相同的线程中。对于run()方法中创建的对象,在这有一个非常惊人的结果。
void Userthread::run()
{
QObject *object1 = new QObject(this); //error, parent must bein the same thread
QObject object2; // OK
QShared Pointer<QObject> object3(new QObject); // OK
}
使用一个互斥量 来保护数据的完整性。
一个互斥量是一中且具有lock()和 unlock() 方法的对象,并记住它是否被锁住。互斥量可在多个线程中访问。如果互斥量没有被锁定, lock() 会立即返回。下一个从其他线程的调用会发现互斥量已经处于锁定状态,然后,lock() 会阻塞线程直到其他线程调用 unlock()。该功能可保证一个代码段在同一时间仅能被一个线程执行。
下面代码显示了怎样使用一个互斥量来确保一个方法是线程安全的。
void Worker::work()
{
this->mutex.lock(); // firstthread can pass, other threads will be blocked here
doWork();
this->mutex.unlock();
}
如果一个线程不能解锁一个互斥量会发生什么情况呢?结果是应用程序会僵死。在上面的例子中,可以会抛出异常且永远不会到达mutex.unlock() 。为了防止这种情况,应该使用 QMutexLocker 。
void Worker::work()
{
QMutexLockerlocker(&mutex); // Locks the mutexand unlocks when locker exits the scope
doWork();
}
这看上去很简单,但互斥会引入新的问题:死锁。当一个线程等待一个互斥量变为解锁,但是该互斥量仍然处于锁定状态,因为占有该互斥量的线程在等待第一个线程解锁该互斥量。结果是一个僵死的应用程序。互斥量用于保证一个方法是线程安全的。大部分Qt方法不是线程安全的,因为当使用互斥量时总是有些性能损失。
在一个方法中并不总是能够加锁和解锁一个互斥量。有时,锁定的范围跨越了数个调用。例如,利用迭代器修改一个容器时需要几个调用组成的序列,这个序列不能被其他线程中断。在这种情况下,利用外部锁就可以保证这个调用序列是被锁定的。利用一个外部锁,锁定的时间可以根据操作的需要进行调整。不好之处是外部锁帮助锁定,但不能强制执行它,因为对象的使用者可能忘记使用它。
使用事件循环来防止数据崩溃
Qt的事件循环对线程间通信是一个非常有价值的工具。每个线程可以拥有自己的事件循环。调用另一个线程中的槽的安全方法就是将此调用放在该线程的事件循环中。这确保了目标对象在启动另一方法前完成了当前正在执行的方法。那么,怎样将一个方法调用放到一个事件循环中呢?Qt有两种方式。一种方式是通过queued信号-槽连接;另一种方式就是利用QCoreApplication::postEvent()发送一个事件。一个queued 信号-槽连接是一种异步执行的信号槽连接。内部实现是基于发送的事件。信号的参数放置到事件循环中,信号方法会立即返回。
连接的槽执行的时间取决于事件循环中的基于事件。通过事件循环通信消除了使用互斥量面临的死锁问题。这就是为什么我们建议使用事件循环而不是使用互斥量锁定一个对象。
处理异步执行
一种获得工作者线程结果的方式是等待该线程停止。然而,在许多情况下,阻塞的等待是不可接受的。另一种方式是通过发送的事件或queued信号和槽来获得异步结果。这产生了一些开销,因为一个操作的结果并不是出现在下一个代码行,而是在一个位于其他地方的槽中。Qt开发者习惯了这种异步行为
该手册提供了一些例子,演示了在Qt中使用线程的三种基本方法。另外两个例子演示了怎样与一个运行中的线程进行通信以及一个 QObject 可被置于另一个线程中,为主线程提供服务。
使用 QThread 使用如上所示。
使用全局的QThreadPool;
使用 QtConcurrent;
与GUI线程进行通信;
在另一个线程的常驻对象为主线程提供服务;
如下 的例子可以单独地进行编译和运行。
例 1: 使用Thread Pool
不停地创建和销毁线程非常耗时,可以使用一个线程池。线程池可以存取线程和获取线程。我们可以使用全局线程池写一个与上面相同的"hello thread" 程序 。我们从QRunnable派生出一个类。在另一个线程中运行的代码必须放在重载的QRunnable::run()方法中。
//Userthreadpool/main.cpp
class Work : publicQRunnable
{
public:
void run()
{
qDebug() <<"data from thread " << QThread::currentThread();
}
};
int main(int argc, char*argv[])
{
QCoreApplication app(argc, argv);
Work work;
work.setAutoDelete(false);
QThreadPool *threadPool =QThreadPool::globalInstance();
threadPool->start(&work);
qDebug() <<"data from GUI thread " << QThread::currentThread();
threadPool->waitForDone();
return 0;
}
在main()中, 我们实例化了Work, 定位于全局的线程池,使用QThreadPool::start()方法。现在,线程池在另一个线程中运行我们的工作。 使用线程池有一些性能上的优势,因为线程在它们结束后没有被销毁,它们被保留在线程池中,等待之后再次被使用。
例 2: 使用QtConcurrent
// Userconcurrent/main.cpp
void Setdata()
{
qDebug() <<"data from thread " << QThread::currentThread();
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc,argv);
QFuture<void> future= QtConcurrent::run(data);
qDebug() <<"data from GUI thread " << QThread::currentThread();
future.waitForFinished();
return 0;
}
我们写一个全局的函数hello()来实现工作者代码。QtConcurrent::run()用于在另一个线程中运行该函数。该结果是QFuture。 QFuture 提供了一个方法叫waitForFinished(), 它阻塞主线程直到计算完成。当所需的数据位于容器中时,QtConcurrent才显示它真正的威力。 QtConcurrent 提供了一些函数能并行地处理这些已经成为容器里元素的一些数据。使用QtConcurrent非常类似于应用一个STL算法到某个STL容器类。QtConcurrent Map是一个非常简短且清晰的例子,它演示了容器中的图片怎么被扩散到所有核中去处理。对于每个阻塞函数,都同时存在一个非阻塞,异步型函数。异步地获取结果是通过QFuture 和QFutureWatcher来实现的。
例3.电子表
我们想创建一个电子表应用程序。该应用程序有一个GUI和一个工作者线程。工作者线程每10毫秒检查一下当前的时间。如果格式化的时间发生了变化,该结果会发送给显示时间的GUI线程当中。
当然, 这是一种过度复杂的方式来设计一个电子表,事实上,一个独立的线程没必要。使用计时器会更好。本例子纯粹是用于教学目的的,演示了从工作者线程向GUI线程进行通信。注意,这种通信方式非常容易,我们仅需要添加一个信号给QThread,然后构建一个queued 信号/槽连接到主线程中。从GUI到工作者线程的方式在下一个例子中演示。
int main(int argc, char *argv[])
{
QApplication app(argc,argv);
// build gui
QWidget widget;
QLabel *label = newQLabel;
QHBoxLayout *layout = newQHBoxLayout(&widget);
layout->addWidget(label);
widget.setWindowTitle("clock");
//instantiate threadobject
ClockThreadclockThread;
QObject::connect(&clockThread, SIGNAL(sendTime(QString)), label,SLOT(setText(QString)), Qt::QueuedConnection);
clockThread.start();
widget.show();
app.exec();
clockThread.quit();
clockThread.wait();
return 0;
}
将clockThread 与标签连接起来。连接必须是一个queued 信号-槽连接,因为我们想将调用放到事件循环当中。
// clock/clockthread.h
class ClockThread : publicQThread
{
Q_OBJECT
signals:
void sendTime(QStringtime);
private:
void run();
QString m_lastTime;
private slots:
void timerHit();
};
从 QThread 派生出一个类,并声明sendTime()信号。
// clock/clockthread.cpp
void ClockThread::run()
{
QTimer timer;
connect(&timer,SIGNAL(timeout()), this, SLOT(timerHit()), Qt::DirectConnection);
timer.setInterval(10);
timer.start(); // puts one event in the threads eventqueue
exec();
timer.stop();
}
void ClockThread::timerHit()
{
QString newTime=QDateTime::currentDateTime().toString("ddd MMMM d yy,hh:mm:ss");
if(m_lastTime != newTime){
m_lastTime =newTime;
emit sendTime(newTime);
}
}
该例子中最值得关注的部分是计时器通过一个直接连接与它的槽相连。默认的连接会产生一个queued 信号-槽连接,因为被连接的对象位于不同的线程。记住,QThread并不位于它创建的线程中。但是,从工作者线程中访问ClockThread::timerHit() 仍然是安全的,因为ClockThread::timerHit()是私有的,且只处理私有变量。QDateTime::currentDateTime() 在Qt文档中并未标记为线程安全的,但是在此例子中,我们可以放心使用,因为我们知道访方法没有会其他的线程中使用。
例 4: 常驻线程
该例子演示了位于工作者线程中的一个QObject接受来自GUI线程的请求,利用一个计时器进行轮询,并不时地将结果返回给GUI线程。实现的工作包括轮询必须实现在一个从QObject派生出的类中。在如下代码中,我们已称该类为 WorkerObject。线程相关的代码已经隐藏在称为Thread类中,派生自QThread. Thread有两个额外的公共成员。launchWorker() 获取工作者对象并将其移到另一个开启了事件循环的线程中。该调用阻塞一小会,直到创建操作完成,使得工作者对象可以在下一行被再次使用。Thread 类的代码短但有点复杂,因此我们只显示怎样使用该类。
// movedobject/main.cpp
int main(int argc, char*argv[])
{
QCoreApplicationapp(argc, argv);
Thread thread;
qDebug() <<"main thread ID: " << app.thread()->currentThreadId();
WorkerObject *worker =new WorkerObject;
thread.launchWorker(worker);
QMetaObject::invokeMethod(worker, "doWork",Qt::QueuedConnection);
QMetaObject::invokeMethod(worker, "startPolling", Qt::QueuedConnection, Q_ARG(int, 500));
//let application produceoutput for 3 seconds and quit
QTimer::singleShot(3000,&app, SLOT(quit()));
app.exec();
thread.stop();
thread.wait();
delete worker;
return 0;
}
QMetaObject::invokeMethod()通过事件循环调用槽。worker对象的方法不应该在对象被移动到另一个线程中直接调用。我们让工作者线程执行一个工作和轮询,并使用一个计时器在3秒后关闭该应用程序。关闭worker需要当心。我们调用 Thread::stop() 退出事件循环。我们等待线程停止,当线程停止后,我们删除worker。
/*********************************************************************************
Qt 线程基础(QThread、QtConcurrent等)
昨晚看Qt的Manual,突然发现下一个版本的Qt中(Qt4.7.4、Qt4.8等)增加了一个特赞的介绍多线程的文章 :
- Thread Basics
注意:
- 该链接以后会失效,但是 到时候你直接看Qt自带Manual就行了
- 本文不是严格的翻译 dbzhang800 2011.06.18
使用线程
基本上有种使用线程的场合:
- 通过利用处理器的多个核使处理速度更快。
- 为保持GUI线程或其他高实时性线程的响应,将耗时的操作或阻塞的调用移到其他线程。
何时使用其他技术替代线程
开发人员使用线程时需要非常小心。启动线程是很容易的,但确保所有共享数据保持一致很难。遇到问题往往很难解决,这是由于在一段时间内它可能只出现一次或只在特定的硬件配置下出现。在创建线程来解决某些问题之前,应该考虑一些替代的技术 :
替代技术
注解
QEventLoop::processEvents()
在一个耗时的计算操作中反复调用QEventLoop::processEvents() 可以防止界面的假死。尽管如此,这个方案可伸缩性并不太好,因为该函数可能会被调用地过于频繁或者不够频繁。
QTimer
后台处理操作有时可以方便地使用Timer安排在一个在未来的某一时刻执行的槽中来完成。在没有其他事件需要处理时,时间隔为0的定时器超时事件被相应
QSocketNotifier
QNetworkAccessManager
QIODevice::readyRead()
这是一个替代技术,替代有一个或多个线程在慢速网络执行阻塞读的情况。只要响应部分的计算可以快速执行,这种设计比在线程中实现的同步等待更好。与线程相比这种设计更不容易出错且更节能(energy efficient)。在许多情况下也有性能优势。
一般情况下,建议只使用安全和经过测试的方案而避免引入特设线程的概念。QtConcurrent 提供了一个将任务分发到处理器所有的核的易用接口。线程代码完全被隐藏在 QtConcurrent 框架下,所以你不必考虑细节。尽管如此,QtConcurrent 不能用于线程运行时需要通信的情况,而且它也不应该被用来处理阻塞操作。
应该使用 Qt 线程的哪种技术?
有时候,你需要的不仅仅是在另一线程的上下文中运行一个函数。您可能需要有一个生存在另一个线程中的对象来为GUI线程提供服务。也许你想在另一个始终运行的线程中来轮询硬件端口并在有关注的事情发生时发送信号到GUI线程。Qt为开发多线程应用程序提供了多种不同的解决方案。解决方案的选择依赖于新线程的目的以及线程的生命周期。
生命周期
开发任务
解决方案
一次调用
在另一个线程中运行一个函数,函数完成时退出线程
编写函数,使用QtConcurrent::run 运行它
派生QRunnable,使用QThreadPool::globalInstance()->start() 运行它
派生QThread,重新实现QThread::run() ,使用QThread::start() 运行它
一次调用
需要操作一个容器中所有的项。使用处理器所有可用的核心。一个常见的例子是从图像列表生成缩略图。
QtConcurrent 提供了map()函你数来将操作应用到容器中的每一个元素,提供了fitler()函数来选择容器元素,以及指定reduce函数作为选项来组合剩余元素。
一次调用
一个耗时运行的操作需要放入另一个线程。在处理过程中,状态信息需要发送会GUI线程。
使用QThread,重新实现run函数并根据需要发送信号。使用信号槽的queued连接方式将信号连接到GUI线程的槽函数。
持久运行
生存在另一个线程中的对象,根据要求需要执行不同的任务。这意味着工作线程需要双向的通讯。
派生一个QObject对象并实现需要的信号和槽,将对象移动到一个运行有事件循环的线程中并通过queued方式连接的信号槽进行通讯。
持久运行
生存在另一个线程中的对象,执行诸如轮询端口等重复的任务并与GUI线程通讯。
同上,但是在工作线程中使用一个定时器来轮询。尽管如此,处理轮询的最好的解决方案是彻底避免它。有时QSocketNotifer是一个替代。
Qt线程基础
QThread是一个非常便利的跨平台的对平台原生线程的抽象。启动一个线程是很简单的。让我们看一个简短的代码:生成一个在线程内输出"hello"并退出的线程。
// hellothread/hellothread.h
class HelloThread : public QThread
{
Q_OBJECT
private:
void run();
};
我们从QThread派生出一个类,并重新实现run方法。
// hellothread/hellothread.cpp
void HelloThread::run()
{
qDebug() << "hello from worker thread " << thread()->currentThreadId();
}
run方法中包含将在另一个线程中运行的代码。在本例中,一个包含线程ID的消息被打印出来。 QThread::start()将在另一个线程中被调用。
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
HelloThread thread;
thread.start();
qDebug() << "hello from GUI thread " << app.thread()->currentThreadId();
thread.wait(); // do not exit before the thread is completed!
return 0;
}
QObject与线程
QObject有线程关联(thread affinity)[如何翻译?关联?依附性?dbzhang800 20110618],换句话说,它生存于一个特定的线程。这意味着,在创建时QObject保存了到当前线程的指针。当事件使用postEvent()被派发时,这个信息变得很有用。事件被放置到相应线程的事件循环中。如果QObject所依附的线程没有事件循环,该事件将永远不会被传递。
要启动事件循环,必须在run()内调用exec()。线程关联可以通过moveToThread()来更改。
如上所述,当从其他线程调用对象的方法时开发人员必须始终保持谨慎。线程关联不会改变这种状况。 Qt文档中将一些方法标记为线程安全。postEvent()就是一个值得注意的例子。一个线程安全的方法可以同时在不同的线程被调用。
通常情况下并不会并发访问的一些方法,在其他线程调用对象的非线程安全的方法在出现造成意想不到行为的并发访问前数千次的访问可能都是工作正常的。编写测试代码不能完全确保线程的正确性,但它仍然是重要的。在Linux上,Valgrind和Helgrind有助于检测线程错误。
QThread的内部结构非常有趣:
- QThread并不生存于执行run()的新线程内。它生存于旧线程中。
- QThread的大多数成员方法是线程的控制接口,并设计成从旧线程中被调用。不要使用moveToThread()将该接口移动到新创建的线程中;调用moveToThread(this)被视为不好的实践。
- exec()和静态方法usleep()、msleep()、sleep()要在新创建的线程中调用。
- QThread子类中定义的其他成员可在两个线程中访问。开发人员负责访问的控制。一个典型的策略是在start()被调用前设置成员变量。一旦工作线程开始运行,主线程不应该操作其他成员。当工作线程终止后,主线程可以再次访问其他成员。这是一个在线程开始前传递参数并在结束后收集结果的便捷的策略。
QObject必须始终和parent在同一个线程。对于在run()中生成的对象这儿有一个惊人的后果:
void HelloThread::run()
{
QObject *object1 = new QObject(this); //error, parent must be in the same thread
QObject object2; // OK
QSharedPointer <QObject> object3(new QObject); // OK
}
使用互斥量保护数据的完整
互斥量是一个拥有lock()和unlock()方法并记住它是否已被锁定的对象。互斥量被设计为从多个线程调用。如果信号量未被锁定lock()将立即返回。下一次从另一个线程调用会发现该信号量处于锁定状态,然后lock()会阻塞线程直到其他线程调用unlock()。此功能可以确保代码段将在同一时间只能由一个线程执行。
使用事件循环防止数据破坏
Qt的事件循环对线程间的通信是一个非常有价值的工具。每个线程都可以有它自己的事件循环。在另一个线程中调用一个槽的一个安全的方法是将调用放置到另一个线程的事件循环中。这可以确保目标对象调用另一个的成员函数之前可以完成当前正在运行的成员函数。
那么,如何才能把一个成员调用放于一个事件循环中? Qt的有两种方法来做这个。一种方法是通过queued信号槽连接;另一种是使用QCoreApplication::postEvent()派发一个事件。queued的信号槽连接是异步执行的信号槽连接。内部实现是基于posted的事件。信号的参数放入事件循环后信号函数的调用将立即返回。
连接的槽函数何时被执行依赖于事件循环其他的其他操作。
通过事件循环通信消除了我们使用互斥量时所面临的死锁问题。这就是我们为什么推荐使用事件循环,而不是使用互斥量锁定对象的原因。
处理异步执行
一种获得一个工作线程的结果的方法是等待线程终止。在许多情况下,一个阻塞等待是不可接受的。阻塞等待的替代方法是异步的结果通过posted事件或者queued信号槽进行传递。由于操作的结果不会出现在源代码的下一行而是在位于源文件其他部分的一个槽中,这会产生一定的开销,因为,但在位于源文件中其他地方的槽。 Qt开发人员习惯于使用这种异步行为工作,因为它非常相似于GUI程序中使用的的事件驱动编程。
版权归原作者 luckyone906 所有, 如有侵权,请联系我们删除。