QT学习笔记18多线程

通常情况下,应用程序都是在一个线程中执行操作。但是,当调用一个耗时操作(例如,大批量I/O或大量矩阵变换等CPU密集操作)时,用户界面常常会冻结。而使用多线程可以解决这一问题。

多线程有以下几个优势:

  • 提高应用程序响应速度。

    这对于图形界面开发的程序尤为重要,当一个操作耗时很长时,整个系统都会等待这个操作,程序就不能响应键盘、鼠标、菜单等操作,而使用多线程技术可将耗时长的操作置于一个新的线程,避免以上问题。

  • 使多CPU系统更加有效。

    当前线程数不大于CPU数目时,操作系统可以调度不同的线程运行于不同的CPU上。

  • 改善程序结构。

    一个既长又复杂的进程可以考虑分为多个线程,成为独立或半独立的运行部分,这样有利于代码的理解和维护。

多线程程序有以下几个特点:

  • 多线程程序的行为无法预期,当多次执行程序时,每一次的结果都可能不同。
  • 多线程的执行顺序无法保证,它与操作系统的调度策略和线程优先级等因素有关。
  • 多线程的切换可能发生在任何时刻、任何地点。
  • 多线程对代码的敏感度高,对代码的细微修改都可能产生意想不到的结果。

基于以上这些特点,为了有效的使用线程,开发人员必须对其进行控制。

1 线程介绍

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

MainWindow::MainWindow(QWidget *parent)

: QMainWindow(parent)

{

QWidget *widget = new QWidget(this);

QVBoxLayout *layout = new QVBoxLayout;

widget->setLayout(layout);

QLCDNumber *lcdNu///mber = 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++));

});

 

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 开启一个新的线程:

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()函数就是新的线程需要执行的代码。在这里就是要执行这个循环,然后发出计算完成的信号。run()是线程的入口,就像main()对于应用程序的作用,使用QThread::start()函数启动一个线程(注意,这里不是run()函数)。再次运行程序,你会发现现在界面已经不会被阻塞了。另外,我们将WorkerThread::deleteLater()函数与WorkerThread::finished()信号连接起来,当线程完成时,系统可以帮我们清除线程实例。这里的finished()信号是系统发出的,与我们自定义的done()信号无关。

 

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


综合示例:


主窗口头文件


#ifndef MYWIDGET_H
#define MYWIDGET_H

#include <QWidget>
#include <QTimer> //定时器头文件
#include "mythread.h" //线程头文件

namespace Ui {
class MyWidget;
}

class MyWidget : public QWidget
{
    Q_OBJECT

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

    void dealTimeout(); //定时器槽函数
    void dealDone(); //线程结束槽函数
    void stopThread(); //停止线程槽函数

private slots:
    void on_pushButton_clicked();

private:
    Ui::MyWidget *ui;

    QTimer *myTimer; //声明变量
    MyThread *thread; //线程对象
};

#endif // MYWIDGET_H


主窗口实现文件:


#include "mywidget.h"
#include "ui_mywidget.h"
#include <QThread>
#include <QDebug>

MyWidget::MyWidget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::MyWidget)
{
    ui->setupUi(this);

    myTimer = new QTimer(this);

    //只要定时器启动,自动触发timeout()
    connect(myTimer, &QTimer::timeout, this, &MyWidget::dealTimeout);

    //分配空间
    thread = new MyThread(this);


    connect(thread, &MyThread::isDone, this, &MyWidget::dealDone);

    //当按窗口右上角x时,窗口触发destroyed()
    connect(this, &MyWidget::destroyed, this, &MyWidget::stopThread);

}

void MyWidget::stopThread()
{
    //停止线程
    thread->quit();
    //等待线程处理完手头动作
    thread->wait();
}

void MyWidget::dealDone()
{
    qDebug() << "it is over";
    myTimer->stop(); //关闭定时器
}

void MyWidget::dealTimeout()
{
    static int i = 0;
    i++;
    //设置lcd的值
    ui->lcdNumber->display(i);
}

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

void MyWidget::on_pushButton_clicked()
{
    //如果定时器没有工作
    if(myTimer->isActive() == false)
    {
        myTimer->start(100);
    }

    //启动线程,处理数据
    thread->start();

}






子线程头文件:

#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QThread>


class MyThread : public QThread
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = 0);

protected:
    //QThread的虚函数
    //线程处理函数
    //不能直接调用,通过start()间接调用
    void run();

signals:
    void isDone();

public slots:
};

#endif // MYTHREAD_H




子线程实现文件:

#include "mythread.h"

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

}

void MyThread::run()
{
    //很复杂的数据处理
    //需要耗时5s
    sleep(5);

    emit isDone();
}


2 多线程的使用

在Qt4.7及以后版本推荐使用以下的工作方式。其主要特点就是利用Qt的事件驱动特性,将需要在次线程中处理的业务放在独立的模块(类)中,由主线程创建完该对象后,将其移交给指定的线程,且可以将多个类似的对象移交给同一个线程。在这个例子中,信号由主线程的QTimer对象发出,之后Qt会将关联的事件放到worker所属线程的事件队列。由于队列连接的作用,在不同线程间连接信号和槽是很安全的。

示例代码如下:

class Worker : public QObject

{

Q_OBJECT

private slots:

void onTimeout()

{

qDebug()<<"Worker::onTimeout get called from?: "

<<QThread::currentThreadId();

}

};

 

int main(int argc, char *argv[])

{

QApplication a(argc, argv);

qDebug()<<"From main thread: "<<QThread::currentThreadId();

 

QThread t;

QTimer timer;

Worker worker;

 

QObject::connect(&timer, SIGNAL(timeout()),

&worker, SLOT(onTimeout()));

         // 启动定时器

timer.start(1000);

     // 将类对象移交个线程

worker.moveToThread(&t);

// 启动线程

t.start();

 

return a.exec();

}

关于Qobject类的connect函数最后一个参数,连接类型:

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

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

  • 队列连接(QueuedConnection)

    当控制权回到接受者所在线程的事件循环时,槽函数被调用。槽函数在接受者所在线程执行。

总结:

* 队列连接:槽函数在接受者所在线程执行

* 直接连接:槽函数在发送者所在线程执行

* 自动连接:二者不在同一线程时,等同于队列连接

 

多线程使用过程中注意事项:

  • 线程不能操作UI对象(从Qwidget直接或间接派生的窗口对象)
  • 需要移动到子线程中处理的模块类,创建的对象的时候不能指定父对象。

综合示例:


主窗口头文件:

#ifndef MYWIDGET_H
#define MYWIDGET_H

#include <QWidget>
#include "mythread.h"
#include <QThread>

namespace Ui {
class MyWidget;
}

class MyWidget : public QWidget
{
    Q_OBJECT

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

    void dealSignal();
    void dealClose();

signals:
    void startThread(); //启动子线程的信号

private slots:
    void on_buttonStart_clicked();

    void on_buttonStop_clicked();

private:
    Ui::MyWidget *ui;
    MyThread *myT;
    QThread *thread;

};

#endif // MYWIDGET_H



主窗口实现文件

#include "mywidget.h"
#include "ui_mywidget.h"
#include <QDebug>


MyWidget::MyWidget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::MyWidget)
{
    ui->setupUi(this);

    //动态分配空间,不能指定父对象
    myT = new MyThread;

    //创建子线程
    thread = new QThread(this);

    //把自定义线程加入到子线程中
    myT->moveToThread(thread);

    connect(myT, &MyThread::mySignal, this, &MyWidget::dealSignal);

    qDebug() << "主线程号:" << QThread::currentThread();

    connect(this, &MyWidget::startThread, myT, &MyThread::myTimeout);


    connect(this, &MyWidget::destroyed, this, &MyWidget::dealClose);

    //线程处理函数内部,不允许操作图形界面


    //connect()第五个参数的作用,连接方式:默认,队列,直接
    //多线程时才有意义
    //默认的时候
    //如果是多线程,默认使用队列
    //如果是单线程, 默认使用直接方式
    //队列: 槽函数所在的线程和接收者一样
    //直接:槽函数所在线程和发送者一样


}

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

void MyWidget::dealClose()
{
    on_buttonStop_clicked();
    delete myT;
}

void MyWidget::dealSignal()
{
    static int i = 0;
    i++;
    ui->lcdNumber->display(i);
}

void MyWidget::on_buttonStart_clicked()
{

    if(thread->isRunning() == true)
    {
        return;
    }

    //启动线程,但是没有启动线程处理函数
    thread->start();
    myT->setFlag(false);

    //不能直接调用线程处理函数,
    //直接调用,导致,线程处理函数和主线程是在同一个线程
    //myT->myTimeout();

    //只能通过 signal - slot 方式调用
    emit startThread();


}

void MyWidget::on_buttonStop_clicked()
{
    if(thread->isRunning() == false)
    {
        return;
    }

    myT->setFlag(true);
    thread->quit();
    thread->wait();
}




子线程头文件:

#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QObject>

class MyThread : public QObject
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = 0);

    //线程处理函数
    void myTimeout();

    void setFlag(bool flag = true);

signals:
    void mySignal();

public slots:

private:
    bool isStop;
};

#endif // MYTHREAD_H




子线程实现文件:

#include "mythread.h"
#include <QThread>
#include <QDebug>
#include <QMessageBox>

MyThread::MyThread(QObject *parent) : QObject(parent)
{
    isStop = false;
}

void MyThread::myTimeout()
{
    while( !isStop )
    {

        QThread::sleep(1);
        emit mySignal();
        //QMessageBox::aboutQt(NULL);

         qDebug() << "子线程号:" << QThread::currentThread();

         if(isStop)
         {
             break;
         }
    }
}

void MyThread::setFlag(bool flag)
{
    isStop = flag;
}




3 使用线程绘图

根据前面讲过的知识,实现以下案例:

在窗口中有一个按钮,当点击按钮之后,在线程中绘制一张图片,然后将绘制好的图片显示到当前窗口中。

实现步骤:

将需要房屋线程中的操作放入单独的一个类中去处理:

class Work : public QObject

{

Q_OBJECT

public:

Work(QObject *parent = 0) : QObject(parent)

{

 

}

 

public slots:

void slotDrawImage()

{

QImage image(600, 600, QImage::Format_ARGB32);

QPainter painter(&image);

QPoint pt[] =

{

QPoint(qrand()%590, qrand()%590),

QPoint(qrand()%590, qrand()%590),

QPoint(qrand()%590, qrand()%590),

QPoint(qrand()%590, qrand()%590),

QPoint(qrand()%590, qrand()%590),

};

painter.drawPolygon(pt, 5);

         // 将画好的图片通过信号发送出去

emit ImageDone(image);

}

signals:

void ImageDone(QImage image);

};

在UI线程中(主线程)中创建Work类对象, 并调用moveToThread函数将操作移入到子线程中取处理.

// 头文件

class MyWidget : public QWidget

{

Q_OBJECT

 

public:

explicit MyWidget(QWidget *parent = 0);

~MyWidget();

 

protected:

void paintEvent(QPaintEvent *);

 

private:

Ui::MyWidget *ui;

QImage m_image;

};

 

// 源文件

MyWidget::MyWidget(QWidget *parent) :

QWidget(parent),

ui(new Ui::MyWidget)

{

ui->setupUi(this);

 

Work* pWork = new Work;

connect(ui->draw, &QPushButton::clicked,

pWork, &Work::slotDrawImage);

 

QThread * pthread = new QThread(this);

// 将操作移入子线程中处理

pWork->moveToThread(pthread);

// 启动子线程

pthread->start();

 

connect(pWork, &Work::ImageDone, [=](QImage image)

{

         // 保存图片

m_image = image;

         // 刷新窗口

update();

});

 

connect(this, &MyWidget::destroyed, [=]()

{

// 退出线程

pthread->quit();

pthread->wait();

delete pWork;

});

}

如果需要在窗口中绘制图形,那么就需要重写paintEvent事件处理函数。通过QPainter对象将子线程中绘制的图片画到当前窗口中。如果需要刷新窗口可以调用update()函数,时间处理器会自动被调用。

void MyWidget::paintEvent(QPaintEvent *e)

{

QPainter p(this);

p.drawImage(0, 0, m_image);

}




综合示例:


主窗口头文件


#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include "mythread.h"
#include <QThread>
#include <QImage>

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

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

    //重写绘图事件
    void paintEvent(QPaintEvent *);

    void getImage(QImage); //槽函数
    void dealClose(); //窗口关闭槽函数

private:
    Ui::Widget *ui;
    QImage image;
    MyThread *myT; //自定义线程对象
    QThread *thread; //子线程
};

#endif // WIDGET_H



主窗口实现文件


#include "widget.h"
#include "ui_widget.h"
#include <QPainter>
#include <QThread>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    //自定义类对象,分配空间,不可以指定父对象
    myT = new MyThread;

    //创建子线程
    thread = new QThread(this);

    //把自定义模块添加到子线程
    myT->moveToThread(thread);

    //启动子线程,但是,并没有启动线程处理函数
    thread->start();

    //线程处理函数,必须通过signal - slot 调用
    connect(ui->pushButton, &QPushButton::pressed, myT, &MyThread::drawImage);
    connect(myT, &MyThread::updateImage, this, &Widget::getImage);

    connect(this, &Widget::destroyed, this, &Widget::dealClose);

}

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

void Widget::dealClose()
{
    //退出子线程
    thread->quit();
    //回收资源
    thread->wait();
    delete myT;

}

void Widget::getImage(QImage temp)
{
    image = temp;
    update(); //更新窗口,间接调用paintEvent()
}

void Widget::paintEvent(QPaintEvent *)
{
    QPainter p(this); //创建画家,指定绘图设备为窗口
    p.drawImage(50, 50, image);
}



子线程头文件


#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QObject>
#include <QImage>

class MyThread : public QObject
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = 0);

    //线程处理函数
    void drawImage();

signals:
    void updateImage(QImage temp);

public slots:
};

#endif // MYTHREAD_H



子线程实现文件



#include "mythread.h"
#include <QPainter>
#include <QPen>
#include <QBrush>
#include <QImage>

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

}

void MyThread::drawImage()
{
    //定义QImage绘图设备
    QImage image(500, 500, QImage::Format_ARGB32);
    //定义画家,指定绘图设备
    QPainter p(&image);


    //定义画笔对象
    QPen pen;
    pen.setWidth(5); //设置宽度
    //把画笔交给画家
    p.setPen(pen);

    //定义画刷
    QBrush brush;
    brush.setStyle(Qt::SolidPattern); //设置样式
    brush.setColor(Qt::red); //设置颜色
    //把画刷交给画家
    p.setBrush(brush);

    //定义5个点
    QPoint a[] =
    {
       QPoint(qrand()%500, qrand()%500),
       QPoint(qrand()%500, qrand()%500),
       QPoint(qrand()%500, qrand()%500),
       QPoint(qrand()%500, qrand()%500),
       QPoint(qrand()%500, qrand()%500)
    };

    p.drawPolygon(a, 5);


    //通过信号发送图片
    emit updateImage(image);

}




  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值