qt多线程学习笔记

多线程

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

多线程有以下几个优势:

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

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

l  使多CPU系统更加有效。

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

l  改善程序结构。

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

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

l  多线程程序的行为无法预期,当多次执行程序时,每一次的结果都可能不同。

l  多线程的执行顺序无法保证,它与操作系统的调度策略和线程优先级等因素有关。

l  多线程的切换可能发生在任何时刻、任何地点。

l  多线程对代码的敏感度高,对代码的细微修改都可能产生意想不到的结果。

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

9.1 线程介绍

在Qt中使用QThread 来管理线程。下面来看一个简单的例子:
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++));
    });

    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 交互的情况。稍懂多线程开发的童鞋都会知道,调试多线程开发简直就是煎熬。

9.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函数最后一个参数,连接类型:

l  自动连接(AutoConnection),默认的连接方式。

n  如果信号与槽,也就是发送者与接受者在同一线程,等同于直接连接;

n  如果发送者与接受者处在不同线程,等同于队列连接。

l  直接连接(DirectConnection)

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

l  队列连接(QueuedConnection)

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

总结:

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

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

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


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

l 线程不能操作UI对象(从Qwidget直接或间接派生的窗口对象)

l 需要移动到子线程中处理的模块类,创建的对象的时候不能指定父对象。

9.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);
}

文件操作

.h
#include <QMainWindow>
#if _MSC_VER >= 1600
#pragma execution_character_set("utf-8")
#endif

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QString>
#include <QStringList>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QByteArray>
#include <QCoreApplication>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

private slots:
    void on_pushButton_clicked();

    void on_pushButton_2_clicked();

private:
    Ui::MainWindow *ui;
    void ListOut(QString msg);
};

#endif // MAINWINDOW_H

.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
#include <QSettings>
#include <QIntValidator>
#include <QDesktopServices>
#include <QThread>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QDesktopServices>
#include <QUrl>
#define appDir QCoreApplication::applicationDirPath()

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    QDir dir;
    dir.mkdir(appDir + "/Log");
    dir.mkdir(appDir + "/Status");

}

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

void MainWindow::on_pushButton_clicked()
{
    QString msg;

    ListOut(msg);
}

void MainWindow::ListOut(QString msg)
{
    //[2018/09/05 14:47:07 073]
    QString current_date_time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss zzz");
    QString message = QString("[%1]%2\r\n").arg(current_date_time).arg(msg);

    QString filename_date_time = QDateTime::currentDateTime().toString("yyyyMMdd");

    //按照天生成
    //    QString filename = QString(appDir + "/Log/ListLog_%1.csv").arg(filename_date_time);
    //按照毫秒生成
    QString filename = appDir + QString("/Log/ListLog_%1.csv").arg(QDateTime::currentDateTime().toString("yyyyMMdd.zzz"));

    QFile file(filename);
    bool flag = file.open(QIODevice::WriteOnly | QIODevice::Append);
    if (flag)
    {
        QTextStream textStream(&file);
        textStream << message;
    }
    else
    {
        QMessageBox::warning(NULL, "Warning", "Error");
    }

    file.flush();
    file.close();
}

void MainWindow::on_pushButton_2_clicked()
{
    QDesktopServices::openUrl(QUrl::fromLocalFile(QApplication::applicationDirPath() + "/Log"));
}







#Java工程师#
全部评论

相关推荐

评论
1
11
分享

创作者周榜

更多
牛客网
牛客企业服务