Qt创建多线程的两种方法

news2024/11/18 20:26:21

Qt创建多线程的两种方法

  • [1] Qt创建多线程的两种方法
    • 1.摘要
    • 2.Qt多线程方法1 继承QThread
      • 2.1写一个继承于QThread的线程
      • 2.2 QThread的几个函数quit、exit、terminate函数
      • 2.3 正确的终止一个线程
      • 2.4 如何正确启动一个线程
      • 2.5 继承QThread的一些总结
    • Qt使用多线程的一些心得——2.继承QObject的多线程使用方法
      • 前言
      • 继承QObject的多线程实现
  • [2] Qt 多线程基础及线程使用方式
    • Qt 多线程操作
    • Qt中使用多线程需要注意:
    • 3.多线程使用:方式一
    • 4.多线程使用:方式二
    • 5.Qt 线程池的使用
  • [3] Qt的线程(两种QThread类的详细使用方式)
    • Qt线程基础
    • Qt提供的线程类
    • 同步QThread的类
    • 可重入性与线程安全
    • QThread和QObjects
    • 使用QtConcurrent类的并行编程

[1] Qt创建多线程的两种方法

原文链接:https://blog.csdn.net/zong596568821xp/article/details/78893360

来源:https://github.com/czyt1988/czyBlog/tree/master/tech/QtThread

1.摘要

Qt有两种多线程的方法,其中一种是继承QThread的run函数,另外一种是把一个继承于QObject的类转移到一个Thread里。 Qt4.8之前都是使用继承QThread的run这种方法,但是Qt4.8之后,Qt官方建议使用第二种方法。两种方法区别不大,用起来都比较方便,但继承QObject的方法更加灵活。这里要记录的是如何正确的创建一个线程,特别是如何正确的退出一个线程。

本文先介绍QThread的普通用法,这个用法可能网上很多文章都介绍过,如果已经了解大可跳过此节,本文重点介绍线程退出的几种方法,根据需求正确的创建和退出线程等问题。

2.Qt多线程方法1 继承QThread

在使用继承QThread的run方法之前需要了解一条规则:

QThread只有run函数是在新线程里的,其他所有函数都在QThread生成的线程里

QThread只有run函数是在新线程里的

QThread只有run函数是在新线程里的

QThread只有run函数是在新线程里的

重要的事情说3遍!!!

如果QThread是在ui所在的线程里生成,那么QThread的其他非run函数都是和ui线程一样的,所以,QThread的继承类的其他函数尽量别要有太耗时的操作,要确保所有耗时的操作都在run函数里。 在UI线程下调用QThread的非run函数(其实也不应该直接调用run函数,而应该使用start函数),和执行普通函数无区别,这时,如果这个函数要对QThread的某个变量进行变更,而这个变量在run函数里也会被用到,这时就需要注意加锁的问题,因为可能这个变量前几毫秒刚刚在run中调用,再调用时已经被另外的线程修改了。

2.1写一个继承于QThread的线程

本文的重点不是教会你继承run写一个多线程,任何有编程基础的5分钟就能学会使用QThread的方法,本文真正要讲的是后面那几节,如如何安全的退出一个线程,如何开启一个临时线程,运行结束马上关闭等问题。如果你对QThread有初步了解,那么可以略过这节,但你最好看看这节后面提出的几个问题。

任何继承于QThread的线程都是通过继承QThread的run函数来实现多线程的,因此,必须重写QThread的run函数,把复杂逻辑写在QThread的run函数中。

看看一个普通继承QThread的例子: 头文件:

#ifndef THREADFROMQTHREAD_H
#define THREADFROMQTHREAD_H
#include <QThread>
 
class ThreadFromQThread : public QThread
{
    Q_OBJECT
signals:
    void message(const QString& info);
    void progress(int present);
public:
    ThreadFromQThread(QObject* par);
    ~ThreadFromQThread();
    void setSomething();
    void getSomething();
    void setRunCount(int count);
    void run();
    void doSomething();
private:
    int m_runCount;
};
 
#endif // THREADFROMQTHREAD_H

cpp文件:

#include "ThreadFromQThread.h"
#include <QDebug>
ThreadFromQThread::ThreadFromQThread(QObject* par) : QThread(par)
,m_runCount(20)
{
 
}
 
ThreadFromQThread::~ThreadFromQThread()
{
    qDebug() << "ThreadFromQThread::~ThreadFromQThread()";
}
 
void ThreadFromQThread::setSomething()
{
    msleep(500);
    QString str = QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId());
    emit message(str);
}
 
void ThreadFromQThread::getSomething()
{
    msleep(500);
    emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
}
 
void ThreadFromQThread::setRunCount(int count)
{
    m_runCount = count;
    emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
}
 
void ThreadFromQThread::run()
{
    int count = 0;
    QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((int)QThread::currentThreadId());
    emit message(str);
    while(1)
    {
        sleep(1);
        ++count;
        emit progress(((float)count / m_runCount) * 100);
        emit message(QString("ThreadFromQThread::run times:%1").arg(count));
        doSomething();
        if(m_runCount == count)
        {
            break;
        }
    }
}
 
void ThreadFromQThread::doSomething()
{
    msleep(500);
    emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));    
}

这个简单的例子有一个Qt类常见的内容,包含了普通方法,信号槽,和一个run函数。这里函数setSomething();进行了500ms的延迟,getSomething同理。这是为了验证在QThread::run()之外调用QThread成员函数不会运行在新线程里。

上面代码用到了QThread::currentThreadId()这是一个静态函数,用于返回当前线程句柄,这个值除了区分以外没有别的用处。

为了验证这个线程,编写一个简单的界面,这个界面主要用于验证如下几个问题:

  • 在UI线程调用setSomething();函数和getSomething();函数会不会卡顿?
  • 在UI线程调用QThread::quit()或QThread::exit()函数会不会停止线程?
  • 在UI线程调用QThread::terminate函数会不会停止线程?
  • 如何正确的退出线程?

2.2 QThread的几个函数quit、exit、terminate函数

为了验证上面这些,编写一个简单的界面如下图所示:

#include "Widget.h"
#include "ui_Widget.h"
#include "ThreadFromQThread.h"
#include <QDebug>
Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //控件初始化
    ui->progressBar->setRange(0,100);
    ui->progressBar->setValue(0);
    ui->progressBar_heart->setRange(0,100);
    ui->progressBar_heart->setValue(0);
    //按钮的信号槽关联
    connect(ui->pushButton_qthread1,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadClicked);
    connect(ui->pushButton_qthread1_setSomething,&QPushButton::clicked
            ,this,&Widget::onButtonQthread1SetSomethingClicked);
    connect(ui->pushButton_qthread1_getSomething,&QPushButton::clicked
            ,this,&Widget::onButtonQthread1GetSomethingClicked);
    connect(ui->pushButton_qthreadQuit,&QPushButton::clicked
            ,this,&Widget::onButtonQthreadQuitClicked);
    connect(ui->pushButton_qthreadTerminate,&QPushButton::clicked
            ,this,&Widget::onButtonQthreadTerminateClicked);
    connect(ui->pushButton_qthreadExit,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadExitClicked);
    connect(ui->pushButton_doSomthing,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadDoSomthingClicked);
    connect(ui->pushButton_qthreadRunLocal,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadRunLoaclClicked);
    //
    connect(ui->pushButton_qobjectStart,&QPushButton::clicked
            ,this,&Widget::onButtonObjectMove2ThreadClicked);
    connect(ui->pushButton_objQuit,&QPushButton::clicked
            ,this,&Widget::onButtonObjectQuitClicked);
    //
    connect(&m_heart,&QTimer::timeout,this,&Widget::heartTimeOut);
    m_heart.setInterval(100);
    //全局线程的创建
    m_thread = new ThreadFromQThread(this);
    connect(m_thread,&ThreadFromQThread::message
            ,this,&Widget::receiveMessage);
    connect(m_thread,&ThreadFromQThread::progress
            ,this,&Widget::progress);
    connect(m_thread,&QThread::finished
            ,this,&Widget::onQThreadFinished);
 
    m_heart.start();
}
 
 
 
Widget::~Widget()
{
    qDebug() << "start destroy widget";
    m_thread->stopImmediately();//由于此线程的父对象是Widget,因此退出时需要进行判断
    m_thread->wait();
    delete ui;
    qDebug() << "end destroy widget";
}
 
void Widget::onButtonQThreadClicked()
{
    ui->progressBar->setValue(0);
    if(m_thread->isRunning())
    {
        return;
    }
    m_thread->start();
}
 
void Widget::progress(int val)
{
    ui->progressBar->setValue(val);
}
 
void Widget::receiveMessage(const QString &str)
{
    ui->textBrowser->append(str);
}
 
void Widget::heartTimeOut()
{
    static int s_heartCount = 0;
    ++s_heartCount;
    if(s_heartCount > 100)
    {
        s_heartCount = 0;
    }
    ui->progressBar_heart->setValue(s_heartCount);
}
 
void Widget::onButtonQthread1SetSomethingClicked()
{
    m_thread->setSomething();
}
 
void Widget::onButtonQthread1GetSomethingClicked()
{
    m_thread->getSomething();
}
 
void Widget::onButtonQthreadQuitClicked()
{
    ui->textBrowser->append("m_thread->quit() but not work");
    m_thread->quit();
}
 
void Widget::onButtonQthreadTerminateClicked()
{
    m_thread->terminate();
}
 
void Widget::onButtonQThreadDoSomthingClicked()
{
    m_thread->doSomething();
}
 
void Widget::onButtonQThreadExitClicked()
{
    m_thread->exit();
}
 
void Widget::onQThreadFinished()
{
    ui->textBrowser->append("ThreadFromQThread finish");
}

界面为上面提到的几个问题提供了按钮, 界面有一个心跳进度条,它是主程序的定时器控制,每100ms触发用于证明主程序的ui线程没有卡死。第二个进度条由线程控制。

点击"QThread run"按钮,触发onButtonQThreadClicked槽,子线程会运行,子线程运行起来后,会打印

../QtThreadTest/ThreadFromQThread.cpp->run,thread id:2900388672

可以确定线程运行的id是2900388672 子线程是个循环,每次循环都会有打印信息:

ThreadFromQThread::run times:1 doSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:2900388672
ThreadFromQThread::run times:2 doSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:2900388672

doSomething是在run函数里调用,其线程id是2900388672,可见这时doSomething函数是运行在子线程里的。

这时,我在界面点击getSomething,setSomething,doSomething会打印:

getSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:3021526784 setSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:3021526784 doSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:3021526784

说明在非run函数里调用QThread的成员函数,并不是在线程里运行(3021526784是widget所在线程)

这时我点击quit,thread并没进行任何处理,QThread在不调用exec()情况下是exit函数和quit函数是没有作用的。

m_thread->quit() but not work

点击terminate按钮,线程马上终止,打印:

ThreadFromQThread finish

动态图如下图所示:

因此可以看出quit和exit函数都不会中途终端线程,要马上终止一个线程可以使用terminate函数,但这个函数存在非常不安定因素,不推荐使用。那么如何安全的终止一个线程呢?

2.3 正确的终止一个线程

最简单的方法是添加一个bool变量,通过主线程修改这个bool变量来进行终止,但这样有可能引起访问冲突,需要加锁 我们需要在原来的头文件加上如下语句:

#include <QMutex>
 
class ThreadFromQThread : public QThread
{
...........
public slots:
    void stopImmediately();
private:
    QMutex m_lock;
    bool m_isCanRun;
.........
};

run函数需要进行修改:

void ThreadFromQThread::stopImmediately()
{
    QMutexLocker locker(&m_lock);
    m_isCanRun = false;
}
 
void ThreadFromQThread::run()
{
    int count = 0;
    m_isCanRun = true;//标记可以运行
    QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((unsigned int)QThread::currentThreadId());
    emit message(str);
    while(1)
    {
        sleep(1);
        ++count;
        emit progress(((float)count / m_runCount) * 100);
        emit message(QString("ThreadFromQThread::run times:%1").arg(count));
        doSomething();
        if(m_runCount == count)
        {
            break;
        }
 
        {
            QMutexLocker locker(&m_lock);
            if(!m_isCanRun)//在每次循环判断是否可以运行,如果不行就退出循环
            {
                return;
            }
        }
    }
}

QMutexLocker可以安全的使用QMutex,以免忘记解锁(有点类似std::unique_ptr),这样每次循环都会看看是否要马上终止。 在线程需要马上退出时,可以在外部调用stopImmediately()函数终止线程,之前的例子可以知道,由于在主线程调用QThread非run()函数的函数都是在主线程运行,因此,在主线程调用类似m_thread->stopImmediately()会几乎马上把线程的成员变量m_isCanRun设置为false(面对多线程问题要用面向过程的思维思考),因此在子线程的run函数的循环中遇到m_isCanRun的判断后就会退出run函数,继承QThread的函数在运行完run函数后就视为线程完成,会发射finish信号。

2.4 如何正确启动一个线程

线程的启动有几种方法,这几种方法设计到它的父对象归属问题,和如何删除他的问题。首先要搞清楚这个线程是否和UI的生命周期一致,直到UI结束线程才结束,还是这个线程只是临时生成,等计算完成就销毁。

第一种情况的线程在创建时会把生成线程的窗体作为它的父对象,这样窗体结束时会自动析构线程的对象。但这时候要注意一个问题,就是窗体结束时线程还未结束如何处理,如果没有处理这种问题,你会发现关闭窗口时会导致程序崩溃。往往这种线程是一个监控线程,如监控某个端口的线程。为了好区分,暂时叫这种叫全局线程,它在UI的生命周期中都存在。

第二种情况是一种临时线程,这种线程一般是突然要处理一个大计算,为了不让UI假死需要触发的线程,这时需要注意一个问题,就是在线程还没计算完成,用户突然终止或变更时如何处理,这种线程往往更多见且更容易出错,如打开一个大文件,显示一个大图片,用户可能看一个大图片还没等图片处理完成又切换到下一个图片,这时绘图线程要如何处理才能顺利解决?为了好区分,暂时叫这种叫局部线程,它在UI的生命周期中仅仅是某时刻才会触发,然后销毁。

这就涉及到如何终止正在执行的线程这个问题!

2.4.1正确的启动一个全局线程(和UI一直存在的线程)
我发现大部分网上的教程都是教你创建一个全局的线程,但往往这种线程用的不多,也比较好管理,需要注意的是程序退出时对线程的处理问题。 在ui的头文件中声明一个线程的指针

widget.h:

ThreadFromQThread* m_thread;

widget.cpp:

class Widget : public QWidget
{
    Q_OBJECT
 
public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();
private slots:
    void onButtonQThreadClicked();
    void onButtonQthread1SetSomethingClicked();
    void onButtonQthread1GetSomethingClicked();
    void onButtonQthreadQuitClicked();
    void onButtonQthreadTerminateClicked();
    void onButtonQThreadDoSomthingClicked();
    void onQThreadFinished();
......
    void progress(int val);
    void receiveMessage(const QString& str);
    void heartTimeOut();
private:
    Ui::Widget *ui;
    ThreadFromQThread* m_thread;
    QTimer m_heart;
......
};

先看窗体生成的构造函数

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
  ,m_objThread(NULL)
{
   
   ui->setupUi(this);
    //控件初始化
    ui->progressBar->setRange(0,100);
    ui->progressBar->setValue(0);
    ui->progressBar_heart->setRange(0,100);
    ui->progressBar_heart->setValue(0);
    //按钮的信号槽关联
    connect(ui->pushButton_qthread1,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadClicked);
    connect(ui->pushButton_qthread1_setSomething,&QPushButton::clicked
            ,this,&Widget::onButtonQthread1SetSomethingClicked);
    connect(ui->pushButton_qthread1_getSomething,&QPushButton::clicked
            ,this,&Widget::onButtonQthread1GetSomethingClicked);
    connect(ui->pushButton_qthreadQuit,&QPushButton::clicked
            ,this,&Widget::onButtonQthreadQuitClicked);
    connect(ui->pushButton_qthreadTerminate,&QPushButton::clicked
            ,this,&Widget::onButtonQthreadTerminateClicked);
    connect(ui->pushButton_doSomthing,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadDoSomthingClicked);
    //心跳的关联
    connect(&m_heart,&QTimer::timeout,this,&Widget::heartTimeOut);
    m_heart.setInterval(100);
    //全局线程的创建
    //全局线程创建时可以把窗体指针作为父对象
    m_thread = new ThreadFromQThread(this);
    //关联线程的信号和槽
    connect(m_thread,&ThreadFromQThread::message
            ,this,&Widget::receiveMessage);//
    connect(m_thread,&ThreadFromQThread::progress
            ,this,&Widget::progress);
    connect(m_thread,&QThread::finished
            ,this,&Widget::onQThreadFinished);
    //UI心跳开始
    m_heart.start();
}

由于是全局存在的线程,因此在窗体创建时就创建线程,可以把线程的父对象设置为窗体,这时需要注意,别手动delete线程指针。用于你的QThread是在Qt的事件循环里面,手动delete会发生不可预料的意外。理论上所有QObject都不应该手动delete,如果没有多线程,手动delete可能不会发生问题,但是多线程情况下delete非常容易出问题,那是因为有可能你要删除的这个对象在Qt的事件循环里还排队,但你却已经在外面删除了它,这样程序会发生崩溃。

如果你确实要删除,请参阅void QObject::deleteLater () [slot]这个槽,这个槽非常有用,尤其是对局部线程来说。后面会经常用到它用于安全的结束线程。

在需要启动线程的地方调用start函数即可启动线程。

void Widget::onButtonQThreadClicked()
{
    ui->progressBar->setValue(0);
    if(m_thread->isRunning())
    {
        return;
    }
    m_thread->start();
}

如果线程已经运行,你重复调用start其实是不会进行任何处理。

一个全局线程就那么简单,要用的时候start一下就行。真正要注意的是如何在ui结束时把线程安全退出。

在widget的析构函数应该这样写:

Widget::~Widget()
{
    qDebug() << "start destroy widget";
    m_thread->stopImmediately();
    m_thread->wait();
    delete ui;
    qDebug() << "end destroy widget";
}

这里要注意的是m_thread->wait();这一句,这一句是主线程等待子线程结束才能继续往下执行,这样能确保过程是单一往下进行的,也就是不会说子线程还没结束完,主线程就destrioy掉了(m_thread的父类是主线程窗口,主线程窗口如果没等子线程结束就destroy的话,会顺手把m_thread也delete这时就会奔溃了),因此wait的作用就是挂起,一直等到子线程结束。

还有一种方法是让QThread自己删除自己,就是在new线程时,不指定父对象,通过绑定**void QObject::deleteLater () [slot]**槽让它自动释放。这样在widget析构时可以免去m_thread->wait();这句。

2.4.2 如何启动一个局部线程(用完即释放的线程)
启动一个局部线程(就是运行完自动删除的线程)方法和启动全局线程差不多,但要关联多一个槽函数,就是之前提到的void QObject::deleteLater () [slot],这个槽函数是能安全释放线程资源的关键(直接delete thread指针不安全)。

简单的例子如下:

void Widget::onButtonQThreadRunLoaclClicked()
{
    //局部线程的创建的创建
    ThreadFromQThread* thread = new ThreadFromQThread(NULL);//这里父对象指定为NULL
    connect(thread,&ThreadFromQThread::message
            ,this,&Widget::receiveMessage);
    connect(thread,&ThreadFromQThread::progress
            ,this,&Widget::progress);
    connect(thread,&QThread::finished
            ,this,&Widget::onQThreadFinished);
    connect(thread,&QThread::finished
            ,thread,&QObject::deleteLater);//线程结束后调用deleteLater来销毁分配的内存
    thread->start();
}

这个例子还是启动之前的线程,但不同的是:

new ThreadFromQThread(NULL);并没有给他指定父对象
connect(thread,&QThread::finished ,thread,&QObject::deleteLater);线程结束后调用deleteLater来销毁分配的内存。 再线程运行完成,发射finished信号后会调用deleteLater函数,在确认消息循环中没有这个线程的对象后会销毁。
但是要注意避免重复点按钮重复调用线程的情况,对于一些需求,线程开启后再点击按钮不会再重新生成线程,一直等到当前线程执行完才能再次点击按钮,这种情况很好处理,加个标记就可以实现,也一般比较少用。

另外更多见的需求是,再次点击按钮,需要终结上次未执行完的线程,重新执行一个新线程。这种情况非常多见,例如一个普通的图片浏览器,都会有下一张图和上一张图这种按钮,浏览器加载图片一般都在线程里执行(否则点击超大图片时图片浏览器会类似卡死的状态),用户点击下一张图片时需要终止正在加载的当前图片,加载下一张图片。你不能要求客户要当前图片加载完才能加载下一张图片,这就几乎沦为单线程了。这时候,就需要终止当前线程,开辟新线程加载下一个图片。

这时,上面的函数将会是大概这个样子的

UI的头文件需要一个成员变量记录正在运行的线程

private slots:
   void onLocalThreadDestroy(QObject* obj);
private:
   QThread* m_currentRunLoaclThread;

运行生成临时线程的函数将变为

void Widget::onButtonQThreadRunLoaclClicked()
{
    //局部线程的创建的创建
    if(m_currentRunLoaclThread)
    {
         m_currentRunLoaclThread->stopImmediately();
    }
    ThreadFromQThread* thread = new ThreadFromQThread(NULL);
    connect(thread,&ThreadFromQThread::message
            ,this,&Widget::receiveMessage);
    connect(thread,&ThreadFromQThread::progress
            ,this,&Widget::progress);
    connect(thread,&QThread::finished
            ,this,&Widget::onQThreadFinished);
    connect(thread,&QThread::finished
            ,thread,&QObject::deleteLater);//线程结束后调用deleteLater来销毁分配的内存
    connect(thread,&QObject::destroyed,this,&Widget::onLocalThreadDestroy);
    thread->start();
    m_currentRunLoaclThread = thread;
}
 
void Widget::onLocalThreadDestroy(QObject *obj)
{
    if(qobject_cast<QObject*>(m_currentRunLoaclThread) == obj)
    {
        m_currentRunLoaclThread = NULL;
    }
}

这里用一个临时变量记录当前正在运行的局部线程,由于线程结束时会销毁自己,因此要通知主线程把这个保存线程指针的临时变量设置为NULL 因此用到了QObject::destroyed信号,在线程对象析构时通知UI把m_currentRunLoaclThread设置为nullptr;

2.5 继承QThread的一些总结

在QThread执行start函数之后,run函数还未运行完毕,再次start会出现什么后果?
答案是:不会发生任何结果,QThread还是继续执行它的run函数,run函数不会被重新调用。虽然在线程未结束时调用start不会出现什么结果,但为了谨慎起见,还是建议在start之前进行判断:

void Widget::onButtonQThreadClicked()
{
    ui->progressBar->setValue(0);
    if(m_thread->isRunning())
    {
        return;
    }
    m_thread->start();
}

这种调用方法估计了解过QThread的都知道

在线程运行过程调用quit函数有什么效果
答案是:不会发生任何效果,QThread不会因为你调用quit函数而退出正在运行到一半的run,正确退出线程的方法上面有介绍。那quit到底有什么用的呢,这要到下篇才能看出它的作用。使用moveToThread方法执行多线程时,这个函数将有大作用。

程序在退出时要判断各线程是否已经退出,没退出的应该让它终止 如果不进行判断,很可能程序退出时会崩溃。如果线程的父对象是窗口对象,那么在窗体的析构函数中,还需要调用wait函数等待线程完全结束再进行下面的析构。

善用QObject::deleteLater 和 QObject::destroyed来进行内存管理 由于多线程环境你不可预料下一步是哪个语句执行,因此,加锁和自动删除是很有用的工具,加锁是通过效率换取安全,用Qt的信号槽系统可以更有效的处理这些问题。

示例代:

–> 见 github

Qt使用多线程的一些心得——2.继承QObject的多线程使用方法

现在Qt官方并不是很推荐继承QThread来实现多线程方法,而是极力推崇继承QObject的方法来实现,当然用哪个方法实现要视情况而定,别弄错了就行,估计Qt如此推崇继承QObject的方法可能是QThread太容易用错的原因。

前言

上一篇介绍了传统的多线程使用方法——继承QThread来实现多线程,这也是很多框架的做法(MFC),但Qt还有一种多线程的实现方法,比直接继承QThread更为灵活,就是直接继承QObject实现多线程。

QObject是Qt框架的基本类,但凡涉及到信号槽有关的类都是继承于QObject。QObject是一个功能异常强大的类,它提供了Qt关键技术信号和槽的支持以及事件系统的支持,同时它提供了线程操作的接口,也就是QObject是可以选择不同的线程里执行的。

QObject的线程转移函数是:void moveToThread(QThread * targetThread) ,通过此函数可以把一个**顶层Object(就是没有父级)**转移到一个新的线程里。

QThread非常容易被新手误用,主要是QThread自身并不生存在它run函数所在的线程,而是生存在旧的线程中,此问题在上一篇重点描述了。由于QThread的这个特性,导致在调用QThread的非run函数容易在旧线程中执行,因此人们发现了一个新的魔改QThread的方法: 人们发现,咦,QThread也继承QObject,QObject有个函数void moveToThread(QThread * targetThread)可以把Object的运行线程转移,那么:(下面是非常不推荐的魔改做法,别用此方法):

class MyThread : public QThread{
public:
    MyThread ()
   {
        moveToThread(this);
   }
……
};

直接把MyThread整个转移到MyThread的新线程中,MyThread不仅run,其它函数也在新线程里了。这样的确可以运行正常,但这并不是QThread设计的初衷,Qt还专门发过一篇文章来吐槽这个做法。

在Qt4.8之后,Qt多线程的写法最好还是通过QObject来实现,和线程的交互通过信号和槽(实际上其实是通过事件)联系。

继承QObject的多线程实现

用QObject来实现多线程有个非常好的优点,就是默认就支持事件循环(Qt的许多非GUI类也需要事件循环支持,如QTimer、QTcpSocket), QThread要支持事件循环需要在QThread::run()中调用QThread::exec()来提供对消息循环的支持,否则那些需要事件循环支持的类都不能正常发送信号,因此如果要使用信号和槽,那就直接使用QObject来实现多线程。

看看Qt官方文档的例子:

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;
public:
    Controller() {
        Worker *worker = new Worker;
        worker->moveToThread(&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 &);
};

使用QObject创建多线程的方法如下:

写一个继承QObject的类,对需要进行复杂耗时逻辑的入口函数声明为槽函数
此类在旧线程new出来,不能给它设置任何父对象
同时声明一个QThread对象,在官方例子里,QThread并没有new出来,这样在析构时就需要调用QThread::wait(),如果是堆分配的话, 可以通过deleteLater来让线程自杀
把obj通过moveToThread方法转移到新线程中,此时object已经是在线程中了
把线程的finished信号和object的deleteLater槽连接,这个信号槽必须连接,否则会内存泄漏
正常连接其他信号和槽(在连接信号槽之前调用moveToThread,不需要处理connect的第五个参数,否则就显示声明用Qt::QueuedConnection来连接)
初始化完后调用’QThread::start()'来启动线程
在逻辑结束后,调用QThread::quit退出线程的事件循环
使用QObject来实现多线程比用继承QThread的方法更加灵活,整个类都是在新的线程中,通过信号槽和主线程传递数据,前篇文章的例子用继承QObject的方法实现的话,代码如下: 头文件(ThreadObject.h):

#include <QObject>
#include <QMutex>
class ThreadObject : public QObject
{
    Q_OBJECT
public:
    ThreadObject(QObject* parent = NULL);
    ~ThreadObject();
    void setRunCount(int count);
    void stop();
signals:
    void message(const QString& info);
    void progress(int present);
public slots:
    void runSomeBigWork1();
    void runSomeBigWork2();
private:
    int m_runCount;
    int m_runCount2;
    bool m_isStop;
 
    QMutex m_stopMutex;
};

cpp文件(ThreadObject.cpp):

#include "ThreadObject.h"
#include <QThread>
#include <QDebug>
#include <QMutexLocker>
#include <QElapsedTimer>
#include <limits>
ThreadObject::ThreadObject(QObject *parent):QObject(parent)
  ,m_runCount(10)
  ,m_runCount2(std::numeric_limits<int>::max())
  ,m_isStop(true)
{
 
}
 
ThreadObject::~ThreadObject()
{
    qDebug() << "ThreadObject destroy";
    emit message(QString("Destroy %1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
}
 
void ThreadObject::setRunCount(int count)
{
    m_runCount = count;
    emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
}
 
void ThreadObject::runSomeBigWork1()
{
    {
        QMutexLocker locker(&m_stopMutex);
        m_isStop = false;
    }
    int count = 0;
    QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((int)QThread::currentThreadId());
    emit message(str);
    int process = 0;
    while(1)
    {
        {
            QMutexLocker locker(&m_stopMutex);
            if(m_isStop)
                return;
        }
        if(m_runCount == count)
        {
            break;
        }
        sleep(1);
        int pro = ((float)count / m_runCount) * 100;
        if(pro != process)
        {
            process = pro;
            emit progress(((float)count / m_runCount) * 100);
            emit message(QString("Object::run times:%1,m_runCount:%2").arg(count).arg(m_runCount2));
        }
        ++count;
    }
}
 
void ThreadObject::runSomeBigWork2()
{
    {
        QMutexLocker locker(&m_stopMutex);
        m_isStop = false;
    }
    int count = 0;
    QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((int)QThread::currentThreadId());
    emit message(str);
    int process = 0;
    QElapsedTimer timer;
    timer.start();
    while(1)
    {
        {
            QMutexLocker locker(&m_stopMutex);
            if(m_isStop)
                return;
        }
        if(m_runCount2 == count)
        {
            break;
        }
        int pro = ((float)count / m_runCount2) * 100;
        if(pro != process)
        {
            process = pro;
            emit progress(pro);
            emit message(QString("%1,%2,%3,%4")
                         .arg(count)
                         .arg(m_runCount2)
                         .arg(pro)
                         .arg(timer.elapsed()));
            timer.restart();
        }
        ++count;
    }
}
 
void ThreadObject::stop()
{
    QMutexLocker locker(&m_stopMutex);
    emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
    m_isStop = true;
}

这个Object有两个耗时函数work1和work2,这两个耗时函数的调用都是通过槽函数触发,同时为了能及时打断线程,添加了一个stop函数,stop函数不是通过信号槽触发,因此需要对数据进行保护,这里用了互斥锁对一个bool变量进行了保护处理,当然会失去一些性能。

主界面的头文件(截取部分代码):

#include <QWidget>
#include <QTimer>
class ThreadFromQThread;
class ThreadObject;
namespace Ui {
class Widget;
}
 
class Widget : public QWidget
{
    Q_OBJECT
 
public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();
signals:
    void startObjThreadWork1();
    void startObjThreadWork2();
private slots:
……
    void onButtonObjectMove2ThreadClicked();
    void onButtonObjectMove2Thread2Clicked();
    void onButtonObjectQuitClicked();
    void onButtonObjectThreadStopClicked();
    
 
 
    void progress(int val);
    void receiveMessage(const QString& str);
    void heartTimeOut();
 
privatevoid startObjThread(); 
private:
    Ui::Widget *ui;
    ……
    ThreadObject* m_obj;
    QThread* m_objThread;
 
};

cpp文件

Widget::~Widget()
{
    qDebug() << "start destroy widget";
   
    if(m_objThread)
    {
        m_objThread->quit();
    }
    m_objThread->wait();
    qDebug() << "end destroy widget";
}
 
//创建线程
void Widget::startObjThread()
{
    if(m_objThread)
    {
        return;
    }
    m_objThread= new QThread();
    m_obj = new ThreadObject();
    m_obj->moveToThread(m_objThread);
    connect(m_objThread,&QThread::finished,m_objThread,&QObject::deleteLater);
    connect(m_objThread,&QThread::finished,m_obj,&QObject::deleteLater);
    connect(this,&Widget::startObjThreadWork1,m_obj,&ThreadObject::runSomeBigWork1);
    connect(this,&Widget::startObjThreadWork2,m_obj,&ThreadObject::runSomeBigWork2);
    connect(m_obj,&ThreadObject::progress,this,&Widget::progress);
    connect(m_obj,&ThreadObject::message,this,&Widget::receiveMessage);
 
    m_objThread->start();
}
 
//调用线程的runSomeBigWork1
void Widget::onButtonObjectMove2ThreadClicked()
{
    if(!m_objThread)
    {
        startObjThread();
    }
 
    emit startObjThreadWork1();//主线程通过信号换起子线程的槽函数
 
    ui->textBrowser->append("start Obj Thread work 1");
}
//调用线程的runSomeBigWork2
void Widget::onButtonObjectMove2Thread2Clicked()
{
    if(!m_objThread)
    {
        startObjThread();
    }
    emit startObjThreadWork2();//主线程通过信号换起子线程的槽函数
 
    ui->textBrowser->append("start Obj Thread work 2");
}
 
//调用线程的中断
void Widget::onButtonObjectThreadStopClicked()
{
    if(m_objThread)
    {
        if(m_obj)
        {
            m_obj->stop();
        }
    }
}

创建线程和官方例子差不多,区别是QThread也是用堆分配,这样,让QThread自杀的槽就一定记得加上,否则QThread就逍遥法外了。

connect(m_objThread,&QThread::finished,m_objThread,&QObject::deleteLater);

加了锁对性能有多大的影响 上例的runSomeBigWork2中,让一个int不停自加1,一直加到int的最大值,为了验证加锁和不加锁的影响,这里对加锁和不加锁运行了两次观察耗时的变化

void ThreadObject::runSomeBigWork2()
{
    {
        QMutexLocker locker(&m_stopMutex);
        m_isStop = false;
    }
    int count = 0;
    QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((int)QThread::currentThreadId());
    emit message(str);
    int process = 0;
    QElapsedTimer timer;
    timer.start();
    while(1)
    {
        {
            QMutexLocker locker(&m_stopMutex);
            if(m_isStop)
                return;
        }
        if(m_runCount2 == count)
        {
            break;
        }
        int pro = ((float)count / m_runCount2) * 100;
        if(pro != process)
        {
            process = pro;
            emit progress(pro);
            emit message(QString("%1,%2,%3,%4")
                         .arg(count)
                         .arg(m_runCount2)
                         .arg(pro)
                         .arg(timer.elapsed()));
            timer.restart();
        }
        ++count;
    }
}

结果如下:

这里没个横坐标的每%1进行了21474837次循环,由统计图可见,
Debug模式下使用了锁后性能下降4倍,
Release模式下下降1.5倍的样子

[2] Qt 多线程基础及线程使用方式

原文链接:https://blog.csdn.net/weixin_49730048/article/details/120791091

Qt 多线程操作

应用程序在某些情况下需要处理比较复杂的逻辑, 如果只有一个线程去处理,就会导致窗口卡顿,无法处理用户的相关操作。这种情况下就需要使用多线程,其中一个线程处理窗口事件,其他线程进行逻辑运算,多个线程各司其职,不仅可以提高用户体验还可以提升程序的执行效率。

Qt中使用多线程需要注意:

Qt的默认线程为窗口线程(主线程):负责窗口事件处理或窗口控件数据的更新;
子线程负责后台的业务逻辑,子线程不能对窗口对象做任何操作,这些事交给窗口线程;
主线程和子线程之间如果进行数据的传递,需要使用信号槽机制
2.线程类QThread
Qt 中提供了一个线程类,通过这个类就可以创建子线程了,Qt 中一共提供了两种创建子线程的方式。

类中常用API函数:

//QThead类常用 API
//构造函数
QThread::QThread(QObject *parent = Q_NULLPTR);
//判断线程中的任务是否处理完毕
bool QThread::isFinished() const;
//判断子线程是不是在执行任务
bool QThread::isRunning() const;

//Qt 可先设置线程优先级
//获取当前线程优先级
Priority QThread::priority() const;
//设置优先级
void QThread::setPriority(Priority priority);   //枚举类型



//退出线程的工作函数
void QThread::exit(int returnCode = 0);
//调用线程退出函数之后,线程不会马上退出,因为当前任务可能没有完成
//等待任务完成后退出线程,通常在exit() 后调用这个函数
bool QThread::wait(unsigned long time = ULONG_MAX);

优先级:
QThread::IdlePriority --> 最低的优先级
QThread::LowestPriority
QThread::LowPriority
QThread::NormalPriority
QThread::HighPriority
QThread::HighestPriority
QThread::TimeCriticalPriority
QThread::InheritPriority --> 最高的优先级, 默认是这个

信号槽:

// 和调用 exit() 效果是一样的
// 代用这个函数之后, 再调用 wait() 函数
[slot] void QThread::quit();
// 启动子线程
[slot] void QThread::start(Priority priority = InheritPriority);
// 线程退出, 可能是会马上终止线程, 一般情况下不使用这个函数
[slot] void QThread::terminate();

// 线程中执行的任务完成了, 发出该信号
// 任务函数中的处理逻辑执行完毕了
[signal] void QThread::finished();
// 开始工作之前发出这个信号, 一般不使用
[signal] void QThread::started();

静态函数

// 返回一个指向管理当前执行线程的QThread的指针
[static] QThread *QThread::currentThread();
// 返回可以在系统上运行的理想线程数 == 和当前电脑的 CPU 核心数相同
[static] int QThread::idealThreadCount();
// 线程休眠函数
[static] void QThread::msleep(unsigned long msecs);	// 单位: 毫秒
[static] void QThread::sleep(unsigned long secs);	// 单位: 秒
[static] void QThread::usleep(unsigned long usecs);	// 单位: 微秒

任务处理函数

// 子线程要处理什么任务, 需要写到 run() 中
[virtual protected] void QThread::run();

run()是一个虚函数,如果想让创建的子线程执行某个任务,需要写一个子类让其继承QThread,并且在子类中重写父类的run()方法,函数体就是任务处理流程。

run()函数是受保护的成员函数,不能类外调用,如果需要通过当前线程对象调用槽函数start()启动子线程,启动后run()函数在线程内部被调用。

3.多线程使用:方式一

1.创建一个线程类的子对象,继承QThread:

class MyThrad:public QThread
{
	......
}

2.重写父类的run()方法,在该函数内部编写子线程要处理的具体业务流程

class MyThread:public QThread
{
	protected:
	void run()
	{
		.........;
	}
}

3.在主线程中创建子线程对象,new一个

MyThread * subThread = new MyThrad;

4.启动子线程,调用start()方法

subThread->start();

不能在类的外部调用run()方法启动子对象,在外部使用start()相当于run()

创建出子线程后,父子线程之间的通信可以通过信号槽的方式,注意:

Qt子线程对象不能操作窗口类型对象,只有主线程才能操作窗口对象。

5.释放线程资源,使用信号槽机制,窗口关闭时释放

connect(this,&QWidget::destroyed,this,[=](){
  //t1 为创建的子线程对象
  gen->quit();
  gen->wait();
  gen->deleteLater();
});

4.多线程使用:方式二

Qt提高的第二种线程的创建方式弥补了第一种的缺点使用更加灵活,单写起来相对复杂一些

具体操作步骤:

1.创建一个新的类,QObject派生

class Mywork :public QObject
{
	....;
}

2.类中添加一个公有的自定义成员函数,函数体就是子线程中执行的业务逻辑

class Mywork :public 	QObject
{
public:
	//自定义函数名、参数
	void working();
}

3.主线程中创建一个 QThread 对象,就是子线程对象

QThread  *sub = new QThread ;

4.在主线程中创建工作的类对象,不要给创建的对象指定父对象

// MyWork* work = new MyWork(this);    
Mywork* work = new Mywork;          // ok

5.将Mywork对象移动到创建的子线程对象中,需要调用QObject类提供的 moveToThread() 方法

/ 如果给work指定了父对象, 这个函数调用就失败了
// 提示: QObject::moveToThread: Cannot move objects with a parent
work->moveToThread(sub);	// 移动到子线程中工作s  

启动子线程,调用 start(), 这时候线程启动了,但是移动到线程中的对象并没有工作

调用 MyWork 类对象的工作函数,让这个函数开始执行,这时候是在移动到的那个子线程中运行的

6.释放线程资源,使用信号槽机制,窗口关闭时释放

//信号槽释放资源
connect(this,&QWidget::destroyed,this,[=](){
 	 //释放创建的子线程对象   
 	 ti->quit();
 	 t1->wait();
  t1->deleteLater();
  
  //释放创建的任务对象
  gen->deleteLater();
  
});

5.Qt 线程池的使用

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

线程池的组成:

1.任务队列: 储存需要处理的任务,由工作的线程来处理这些任务

​ 通过线程池提供的API函数,将一个待处理的任务添加到任务列表,或者从任务列表队伍中删除,已处理的任务会被从任务队列中删除

​ 线程池的使用者,及往任务队列添加任务的线程就是生产者线程

2.工作的线程:(任务队列中的消费者),N个

​ 线程池中维护一定数量的工作线程,作用是不停的读任务队列,从里面取出任务并处理

​ 如果任务队列为空,工作的线程将会被阻塞(使用条件变量/信号量阻塞)

​ 如果阻塞之后有了新的任务,由生产者将阻塞解除,工作线程开始工作

3.管理者线程:(不处理任务队列中的任务),1个

​ 作用:周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测

​ 任务过多时,适当创建新的工作线程

​ 任务过少时,适当销毁一些工作线程

QRunnable

使用线程池需要先创建任务,添加到线程池中的任务 需要QRunnable类型,因此需要创建子类继承QRunnable类,然后重写run()方法,在该函数编写在线程池中执行的任务,并将子类对象传递给线程池,这样线程就可以被线程池中的某个工作线程处理掉。

QRunnable常用函数:

// 在子类中必须要重写的函数, 里边是任务的处理流程
[pure virtual] void QRunnable::run();

// 参数设置为 true: 这个任务对象在线程池中的线程中处理完毕, 这个任务对象就会自动销毁
// 参数设置为 false: 这个任务对象在线程池中的线程中处理完毕, 对象需要程序猿手动销毁
void QRunnable::setAutoDelete(bool autoDelete);
// 获取当前任务对象的析构方式,返回true->自动析构, 返回false->手动析构
bool QRunnable::autoDelete() const;

1.创建一个要添加到线程池中的任务类

class MyWork:public QObject , public QRunnable
{
	Q_OBJECT
public:
	explicit MyWork (QObject *parent = nullptr)
	{
		//执行任务完毕,该对象自动销毁
		setAutoDelete(true);
	}
~MyWork();

void run() override();
}

在上面的示例中 MyWork 类是一个多重继承,如果需要在这个任务中使用 Qt 的信号槽机制进行数据的传递就必须继承 QObject 这个类,如果不使用信号槽传递数据就可以不继承了,只继承 QRunnable 即可。

QThreadPool

Qt 中的 QThreadPool 类管理了一组 QThreads, 里边还维护了一个任务队列。QThreadPool 管理和回收各个 QThread 对象,以帮助减少使用线程的程序中的线程创建成本。==每个Qt应用程序都有一个全局 QThreadPool 对象,可以通过调用 globalInstance() 来访问它。==也可以单独创建一个 QThreadPool 对象使用。

线程池常用API函数:

// 获取和设置线程中的最大线程个数
int maxThreadCount() const;
void setMaxThreadCount(int maxThreadCount);

// 给线程池添加任务, 任务是一个 QRunnable 类型的对象
// 如果线程池中没有空闲的线程了, 任务会放到任务队列中, 等待线程处理
void QThreadPool::start(QRunnable * runnable, int priority = 0);
// 如果线程池中没有空闲的线程了, 直接返回值, 任务添加失败, 任务不会添加到任务队列中
bool QThreadPool::tryStart(QRunnable * runnable);

// 线程池中被激活的线程的个数(正在工作的线程个数)
int QThreadPool::activeThreadCount() const;

// 尝试性的将某一个任务从线程池的任务队列中删除, 如果任务已经开始执行就无法删除了
bool QThreadPool::tryTake(QRunnable *runnable);
// 将线程池中的任务队列里边没有开始处理的所有任务删除, 如果已经开始处理了就无法通过该函数删除了
void QThreadPool::clear();

// 在每个Qt应用程序中都有一个全局的线程池对象, 通过这个函数直接访问这个对象
static QThreadPool * QThreadPool::globalInstance();

runnable);

// 线程池中被激活的线程的个数(正在工作的线程个数)
int QThreadPool::activeThreadCount() const;
// 尝试性的将某一个任务从线程池的任务队列中删除, 如果任务已经开始执行就无法删除了
bool QThreadPool::tryTake(QRunnable *runnable);
// 将线程池中的任务队列里边没有开始处理的所有任务删除, 如果已经开始处理了就无法通过该函数删除了
void QThreadPool::clear();
// 在每个Qt应用程序中都有一个全局的线程池对象, 通过这个函数直接访问这个对象
static QThreadPool * QThreadPool::globalInstance();

一般情况下,我们不需要在 Qt 程序中创建线程池对象,直接使用 Qt 为每个应用程序提供的线程池全局对象即可。得到线程池对象之后,调用 start() 方法就可以将一个任务添加到线程池中,这个任务就可以被线程池内部的线程池处理掉了,使用线程池比自己创建线程的这种多种多线程方式更加简单和易于维护。

[3] Qt的线程(两种QThread类的详细使用方式)

原文链接:https://blog.csdn.net/m0_60259116/article/details/128206276

Qt提供QThread类以进行多任务处理。与多任务处理一样,Qt提供的线程可以做到单个线程做不到的事情。例如,网络应用程序中,可以使用线程处理多种连接器。

QThread继承自QObject类,且提供QMutex类以实现同步。线程和进程共享全局变量,可以使用互斥体对改变后的全局变量值实现同步。因此,必须编辑全局数据时,使用互斥体实现同步,其它进程则不能改变或浏览全局变量值。

什么是互斥体?

互斥体实现了“互相排斥”(mutual exclusion)同步的简单形式(所以名为互斥体(mutex))。互斥体禁止多个线程同时进入受保护的代码“临界区”(critical section)。

在任意时刻,只有一个线程被允许进入代码保护区。任何线程在进入临界区之前,必须获取(acquire)与此区域相关联的互斥体的所有权。如果已有另一线程拥有了临界区的互斥体,其他线程就不能再进入其中。这些线程必须等待,直到当前的属主线程释放(release)该互斥体。

什么时候需要使用互斥体呢?

互斥体用于保护共享的易变代码,也就是,全局或静态数据。这样的数据必须通过互斥体进行保护,以防止它们在多个线程同时访问时损坏。

Qt线程基础

QThread的创建和启动

class MyThread : public QThread
{
    Q_OBJECT
protected:
    void run();
};
 
void MyThread :: run(){
    ...
}

如上述代码所示,如果要创建线程,则必须继承QThread类。MyThread使用成员函数run()才会实现线程。

Qt提供的线程类

在这里插入图片描述

同步QThread的类

为了同步线程,Qt提供了QMutex、QReadWriteLock、QSemaphore和QWaitCondition类。主线程等待与其他线程的中断时,必须进行同步。例如:两个线程同时访问共享变量,那么可能得不到预想的结果。因此,两个线程访问共享变量时,必须进行同步。

  1. 一个线程访问指定的共享变量时,为了禁止其他线程访问,QMutex提供了类似锁定装置的功能。互斥体激活状态下,线程不能同时访问共享变量,必须在先访问的线程完成访问后,其他线程才可以继续访问。
  2. 一个线程访问互斥体锁定的共享变量期间,如果其他线程也访问此共享变量,那么该线程将会一直处于休眠状态,直到正在访问的线程结束访问。这称为线程安全。
  3. QReadWriteLock和QMutex的功能相同,区别在于,QReadWriteLock对数据的访问分为读访问和写访问。很多线程频繁访问共享变量时,与QMetex相对,使用QReadWriteLock更合适。
  4. QSemaphore拥有和QMutex一样的同步功能,可以管理多个按数字识别的资源。QMutex只能管理一个资源,但如果使用QSemaphore,则可以管理多个按号码识别的资源。
  5. 条件符合时,QWaitCondition允许唤醒线程。例如,多个线程中某个线程被阻塞时,通过QWaitCondition提供的函数wakeOne()和wakeAll()可以唤醒该线程。

可重入性与线程安全

  • 可重入性:两个以上线程并行访问时,即使不按照调用顺序重叠运行代码,也必须保证结果;
  • 线程安全:线程并行运行的情况下,虽然保证可以使程序正常运行,但访问静态空间或共享(堆等内存对象)对象时,要使用互斥体等机制保证结果。
    一个线程安全的函数不一定是可重入的;一个可重入的函数缺也不一定是线程安全的!

可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。

编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段对其加以保护。若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个线程调用此函数时,很有可能使有关全局变量变为不可知状态。

满足下列条件的函数多数是不可重入的:

  • 函数体内使用了静态的数据结构和全局变量,若必须访问全局变量,利用互斥信号量来保护全局变量;
  • 函数体内调用了malloc()或者free()函数;
  • 函数体内调用了标准I/O函数。

常见的不可重入函数有:

  • printf ——–引用全局变量stdout
  • malloc ——–全局内存分配表
  • free ——–全局内存分配表
    也就是说:本质上,可重入性与C++类或者没有全局静态变量的函数相似,由于只能访问自身所有的数据变量区域,所以即使有两个以上线程访问,也可以保证安全性。

QThread和QObjects

QThread类继承自QObjects类。因此,线程开始或结束时,QThread类发生发送信号事件。信号与槽的功能是QThread类从QObject类继承的,可以通过信号与槽处理开始或结束等操作,所以可以实现多线程。QObject是基于QTimer、QTcpSocket、QUdpSocket和QProcess之类的非图形用户界面的子类。

基于非图形用户界面的子类可以无线程操作。单一类运行某功能时,可以不需要线程。但是,运行单一类的目标程序的上级功能时,则必须通过线程实现。

线程A和线程B没有结束的情况下,应设计使主线程时间循环不结束;而若线程A迟迟不结束而导致主线程循环也迟迟不能结束,故也要防止线程A没有在一定时间内结束。

处理QThread的信号和槽的类型
Qt提供了可以决定信号与槽类型的枚举类,以在线程环境中适当处理事物。

在这里插入图片描述

使用QtConcurrent类的并行编程

QtConcurrent类提供多线程功能,不使用互斥体、读写锁、等待条件和信号量等低级线程。使用QtConcurrent创建的程序会根据进程数自行调整使用的线程数。

QThread类
简述
QThread类提供了与系统无关的线程。

QThread代表在程序中一个单独的线程控制。线程在run()中开始执行,默认情况下,run()通过调用exec()启动事件循环并在线程里运行一个Qt的事件循环。

详细描述
QThread类可以不受平台影响而实现线程。QThread提供在程序中可以控制和管理线程的多种成员函数和信号/槽。通过QThread类的成员函数start()启动线程。

QThread通过信号函数started()和finished()通知开始和结束,并查看线程状态;可以使用isFinished()和isRunning()来查询线程的状态;使用函数exit()和quit()可以结束线程。

如果使用多线程,有时需要等到所有线程终止。此时,使用函数wait()即可。线程中,使用成员函数sleep()、msleep()和usleep()可以暂停秒、毫秒及微秒单位的线程。

一般情况下,wait()和sleep()函数应该不需要,因为Qt是一个事件驱动型框架。考虑监听finished()信号来取代wait(),使用QTimer来取代sleep()。

静态函数currentThreadId()和currentThread()返回标识当前正在执行的线程。前者返回该线程平台特定的ID,后者返回一个线程指针。

要设置线程的名称,可以在启动线程之前调用setObjectName()。如果不调用setObjectName(),线程的名称将是线程对象的运行时类型(QThread子类的类名)。

线程管理
可以将常用的接口按照功能进行以下分类:

线程启动

void start(Priority priority = InheritPriority) [slot]
调用后会执行run()函数,但在run()函数执行前会发射信号started(),操作系统将根据优先级参数调度线程。如果线程已经在运行,那么这个函数什么也不做。优先级参数的效果取决于操作系统的调度策略。特别是那些不支持线程优先级的系统优先级将会被忽略(例如在Linux中,更多细节请参考http://linux.die.net/man/2/sched_setscheduler)。

线程执行

int exec() [protected]
进入事件循环并等待直到调用exit(),返回值是通过调用exit()来获得,如果调用成功则范围0。

void run() [virtual protected]
线程的起点,在调用start()之后,新创建的线程就会调用这个函数,默认实现调用exec(),大多数需要重新实现这个函数,便于管理自己的线程。该方法返回时,该线程的执行将结束。

线程退出

void quit() [slot]
告诉线程事件循环退出,返回0表示成功,相当于调用了QThread::exit(0)。

void exit(int returnCode = 0)
告诉线程事件循环退出。 调用这个函数后,线程离开事件循环后返回,QEventLoop::exec()返回returnCode,按照惯例,0表示成功;任何非0值表示失败。

void terminate() [slot]
终止线程,线程可能会立即被终止也可能不会,这取决于操作系统的调度策略,使用terminate()之后再使用QThread::wait(),以确保万无一失。当线程被终止后,所有等待中的线程将会被唤醒。

警告:此函数比较危险,不鼓励使用。线程可以在代码执行的任何点被终止。线程可能在更新数据时被终止,从而没有机会来清理自己,解锁等等。。。总之,只有在绝对必要时使用此函数。

void requestInterruption()
请求线程的中断。该请求是咨询意见并且取决于线程上运行的代码,来决定是否及如何执行这样的请求。此函数不停止线程上运行的任何事件循环,并且在任何情况下都不会终止它。

线程等待

void msleep(unsigned long msecs) [static]     //强制当前线程睡眠msecs毫秒
 
void sleep(unsigned long secs) [static]     //强制当前线程睡眠secs秒
 
void usleep(unsigned long usecs) [static]     //强制当前线程睡眠usecs微秒
 
bool wait(unsigned long time = ULONG_MAX)     //线程将会被阻塞,等待time毫秒。和sleep不同的是,如果线程退出,wait会返回。

线程状态

bool isFinished() const     //线程是否结束
 
bool isRunning() const     //线程是否正在运行
 
bool isInterruptionRequested() const     //如果线程上的任务运行应该停止,返回true。可以使用requestInterruption()请求中断。 
 
//此函数可用于使长时间运行的任务干净地中断。从不检查或作用于该函数返回值是安全的,但是建议在长时间运行的函数中经常这样做。注意:不要过于频繁调用,以保持较低的开销。

线程优先级

void setPriority(Priority priority) 

设置正在运行线程的优先级。如果线程没有运行,此函数不执行任何操作并立即返回。使用的start()来启动一个线程具有特定的优先级。优先级参数可以是QThread::Priority枚举除InheritPriortyd的任何值。
在这里插入图片描述
QThread类使用方式
QThread的使用方法有如下两种:

QObject::moveToThread()
继承QThread类
QObject::moveToThread
方法描述:

定义一个继承于QObject的worker类,在worker类中定义一个槽slot函数doWork(),这个函数中定义线程需要做的工作;
在要使用线程的controller类中,新建一个QThread的对象和woker类对象,使用moveToThread()方法将worker对象的事件循环全部交由QThread对象处理;
建立相关的信号函数和槽函数进行连接,然后发出信号触发QThread的槽函数,使其执行工作。
例子:

#ifndef WORKER_H
#define WORKER_H
#include <QObject>
#include<QDebug>
#include<QThread>
class Worker:public QObject                    //work定义了线程要执行的工作
{
    Q_OBJECT
public:
    Worker(QObject* parent = nullptr){}
public slots:
    void doWork(int parameter)                        //doWork定义了线程要执行的操作
    {
        qDebug()<<"receive the execute signal---------------------------------";
        qDebug()<<"     current thread ID:"<<QThread::currentThreadId();
       for(int i = 0;i!=1000000;++i)
       {
        ++parameter;
       }
       qDebug()<<"      finish the work and sent the resultReady signal\n";
       emit resultReady(parameter);           //emit啥事也不干,是给程序员看的,表示发出信号发出信号
    }
 
signals:
    void resultReady(const int result);               //线程完成工作时发送的信号
};
 
#endif // WORKER_H
#ifndef CONTROLLER_H
#define CONTROLLER_H
#include <QObject>
#include<QThread>
#include<QDebug>
class Controller : public QObject            //controller用于启动线程和处理线程执行结果
{
    Q_OBJECT
    QThread workerThread;
public:
    Controller(QObject *parent= nullptr);
    ~Controller();
public slots:
    void handleResults(const int rslt)                        //处理线程执行的结果
    {
        qDebug()<<"receive the resultReady signal---------------------------------";
        qDebug()<<"     current thread ID:"<<QThread::currentThreadId()<<'\n';
        qDebug()<<"     the last result is:"<<rslt;
    }
signals:
    void operate(const int);                        //发送信号触发线程
};
 
#endif // CONTROLLER_H
#include "controller.h"
#include <worker.h>
Controller::Controller(QObject *parent) : QObject(parent)
{
    Worker *worker = new Worker;
    worker->moveToThread(&workerThread);            //调用moveToThread将该任务交给workThread
 
    connect(this, SIGNAL(operate(const int)), worker, SLOT(doWork(int)));            //operate信号发射后启动线程工作
    connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);            //该线程结束时销毁
    connect(worker, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));            //线程结束后发送信号,对结果进行处理
 
    workerThread.start();                //启动线程
    qDebug()<<"emit the signal to execute!---------------------------------";
    qDebug()<<"     current thread ID:"<<QThread::currentThreadId()<<'\n';
    emit operate(0);
}
 
Controller::~Controller()        //析构函数中调用quit()函数结束线程
{
    workerThread.quit();
    workerThread.wait();
}

继承QThread类
方法描述

自定义一个继承QThread的类MyThread,重载MyThread中的run()函数,在run()函数中写入需要执行的工作;
调用start()函数来启动线程。
例子:

#ifndef MYTHREAD_H
#define MYTHREAD_H
#include<QThread>
#include<QDebug>
class MyThread : public QThread
{
    Q_OBJECT
public:
    MyThread(QObject* parent = nullptr);
signals:                //自定义发送的信号
    void myThreadSignal(const int);
public slots:                //自定义槽
    void myThreadSlot(const int);
protected:
    void run() override;
};
 
#endif // MYTHREAD_H
#include "mythread.h"
 
MyThread::MyThread(QObject *parent)
{
 
}
 
void MyThread::run()
{
    qDebug()<<"myThread run() start to execute";
    qDebug()<<"     current thread ID:"<<QThread::currentThreadId()<<'\n';
    int count = 0;
    for(int i = 0;i!=1000000;++i)
    {
     ++count;
    }
    emit myThreadSignal(count);
    exec();
}
 
void MyThread::myThreadSlot(const int val)
{
    qDebug()<<"myThreadSlot() start to execute";
    qDebug()<<"     current thread ID:"<<QThread::currentThreadId()<<'\n';
    int count = 888;
    for(int i = 0;i!=1000000;++i)
    {
     ++count;
    }
}
#include "controller.h"
#include <mythread.h>
Controller::Controller(QObject *parent) : QObject(parent)
{
    myThrd = new MyThread;
    connect(myThrd,&MyThread::myThreadSignal,this,&Controller::handleResults);
    connect(myThrd, &QThread::finished, this, &QObject::deleteLater);            //该线程结束时销毁
    connect(this,&Controller::operate,myThrd,&MyThread::myThreadSlot);
 
    myThrd->start();
    QThread::sleep(5);
    emit operate(999);
}
 
Controller::~Controller()
{
    myThrd->quit();
    myThrd->wait();
}

两种方法的比较

两种方法来执行线程都可以,随便你的喜欢。不过看起来第二种更加简单,容易让人理解。不过我们的兴趣在于这两种使用方法到底有什么区别?其最大的区别在于:

moveToThread方法,是把我们需要的工作全部封装在一个类中,将每个任务定义为一个的槽函数,再建立触发这些槽的信号,然后把信号和槽连接起来,最后将这个类调用moveToThread方法交给一个QThread对象,再调用QThread的start()函数使其全权处理事件循环。于是,任何时候我们需要让线程执行某个任务,只需要发出对应的信号就可以。其优点是我们可以在一个worker类中定义很多个需要做的工作,然后发出触发的信号线程就可以执行。相比于子类化的QThread只能执行run()函数中的任务,moveToThread的方法中一个线程可以做很多不同的工作(只要发出任务的对应的信号即可)。

子类化QThread的方法,就是重写了QThread中的run()函数,在run()函数中定义了需要的工作。这样的结果是,我们自定义的子线程调用start()函数后,便开始执行run()函数。如果在自定义的线程类中定义相关槽函数,那么这些槽函数不会由子类化的QThread自身事件循环所执行,而是由该子线程的拥有者所在线程(一般都是主线程)来执行。如果你不明白的话,请看,第二个例子中,子类化的线程的槽函数中输出当前线程的ID,而这个ID居然是主线程的ID!!事实的确是如此,子类化的QThread只能执行run()函数中的任务直到run()函数退出,而它的槽函数根本不会被自己的线程执行。

QThread的信号与槽

启动或终止线程时,QThread提供了信号与槽。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/537540.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

什么是边缘计算盒子?边缘计算盒子可以做什么?一文带你了解边缘计算云服务器 ECS

上文&#xff0c;我们已经为大家介绍了什么是边缘计算、边缘计算的诞生、以及边缘计算与CDN之间的关系&#xff0c;感兴趣的小伙伴欢迎阅读以往文章&#xff1a; 边缘计算节点是啥&#xff1f;边缘计算与CDN有什么关系&#xff1f;一文带你了解边缘计算节点BEC&#xff08;1&am…

在线PS软件有哪些不错的推荐

许多新的UI设计合作伙伴非常关心在线ps工具的选择。现在市场上有各种各样的ps网页替代工具&#xff0c;数量众多&#xff0c;令人眼花缭乱。本文简要介绍了10个在线PS工具&#xff0c;我相信一定有一个适合你&#xff01; 1.即时设计 即时设计是一款在线 UI 设计工具&#xf…

渗透测试--5.1.Crunch创建密码字典

目录 1.crunch简介 2.常用命令 3.使用实例 &#xff08;1&#xff09;.生成字母组合 &#xff08;2&#xff09;. 生成缺位的手机号码 &#xff08;3&#xff09;.生成pass01-pass99所有组合 &#xff08;4&#xff09;.生成六位密码&#xff0c;其中前四位为pass&#xf…

docker-compose部署开发环境

docker-compose部署开发环境 部署nginx欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义…

Java内部类与匿名类

内部类 定义&#xff1a; 一个类的内部又完整的嵌套了另一个类结构&#xff0c;被嵌套的类就被我们称为内部类&#xff0c;嵌套内部类的类被我们称为外部类 //外部类 class Outer {//内部类class Inner{} }package Innerclass;//外部其他类 public class c1 { }class Outer {/…

数据结构--线段树--洛谷模板线段树1-2

写在前面&#xff1a; 学习之前需要知道以下内容&#xff1a; 1. 递归 2. 二叉树 文章目录 线段树介绍用途建树修改单点修改区间修改 查询 代码实现。建树更新lazy传递查询优化一下 练习洛谷 P3372 【模板】线段树 1题目描述题解 【模板】线段树 2题目描述题解 线段树 介绍 …

生产流程图怎么制作?思路提供

生产流程图是一种图表&#xff0c;用来展示生产流程中的各个环节及其顺序。这种图表可以帮助企业管理者更好地了解生产过程中的各个环节&#xff0c;从而更好地进行管理和优化。生产流程图通常包括各个生产环节的名称、所需时间、参与人员、设备和工具等信息。 在制作生产流程图…

虚拟机 03 补充 redis的命令

Redis的操作命令&#xff1a; Redis的启动命令&#xff1a;redis-server redis.conf 进入redis的客户端操作命令&#xff1a;redis-cli -p 6379 退出redis客户端的操作命令&#xff1a;exit或者ctrlc 关闭redis的命令&#xff1a;redis-cli -p 6379 shutdown Redis数据操作命令…

免费优质的网页设计素材网站推荐

找到一个免费优质的网页设计素材网站并不容易。 有些网站要么需要网站的会员&#xff0c;要么设计素材质量差。本文整理总结了10个免费的网页设计材料网站&#xff0c;希望给大家带来一些帮助。 即时设计资源社区 即时设计资源社区是由在线协同设计软件即时设计推出的设计社…

2023乡村振兴三点:粮食安全,数字乡村,特色产业

2023乡村振兴三条&#xff1a;粮食安全&#xff0c;数字乡村&#xff0c;特色产业 人工智能总趋势在减少就业 乡村生活综合服务领域&#xff5e; 可能是以后吸纳劳动力的重要领域 农业现代化最后在实现中… 趣讲大白话&#xff1a;人没事干&#xff0c;咋办 【趣讲信息科技168期…

基于 debian 发行版本的密码设置策略

密码过期时间设置 修改 /etc/login.defs 中的选项 PASS_MAX_DAYS 99999 代表不过期 我们将该值改为 90 密码复杂度设置 需要安装 pam 扩展包 libpam-pwquality 或 libpam-cracklib 。 这里选择安装 libpam-cracklib sudo apt updatesudo apt install libpam-cracklib查看…

虚拟机 03 redis安装和启动服务

redis在linux上的安装&#xff1a; 01.先找到redis的压缩文件 这里的是gz格式的。 02.在linux系统中&#xff0c;先去切换到根目录先 使用命令 cd / 再切换到usr/local/src中 直接把压缩文件拖入到linux系统的命令行中 src文件中会出现redis-5.0.4.tar.gz的文件 03.对这个文…

arm作业

用for循环实现1~100之间的和13BA for(i1;i<100;i) { sum sumi; }

面试7轮,结果对接的HR离职了……

面试鹅厂总共进行了7轮&#xff0c;还没有结果。先是面了三轮&#xff0c;然后hr告诉她没hc了&#xff0c;然后又推荐了另外一个岗位&#xff0c;又面了四轮。然后追问HR后续的进展&#xff0c;她告诉我已经离职了&#xff0c;让我联系另外一个同事。 这么说吧&#xff0c;大厂…

密码学A5算法VHDL设计及仿真验证

A5算法是一种用于GSM(Global System for Mobile Communications)移动通信标准的加密算法。它是一种对称密钥算法,使用一个64位密钥和一个22位帧号作为输入,生成一个228位的伪随机序列,该序列被用于加密语音和数据通信。 A5算法由三个线性反馈移位寄存器(LFSR)组成,每个…

创建一个Go项目

创建一个Go项目 1.创建项目 package mainfunc main() {println("你好啊&#xff0c;简单点了&#xff01;") }如果是本地的话可以采用go run 项目名的方式。 可以采用go run --work 项目名的方式&#xff0c;此时可以展示日志信息。 如果是只编译的话 go build 项…

1.Buffer_Overflow-1.Basic_Jump

github上面的练习题 git clone https://github.com/Adamkadaban/LearnPwn 然后开始做 先进行 readelf 然后进行执行看看 是怎么回事 ./buf1发现就是一个输入和输出 我们checksec看看 发现stack 保护关闭 开启了NX保护 我们进入ida64看看反汇编 我习惯先看看字符串 SHITF…

MATLAB 之 函数文件、特殊形式的函数和程序调试与优化

文章目录 一、函数文件1. 函数文件的基本结构2. 函数调用2.1 函数调用的格式2.2 函数的递归调用2.3 函数参数的可调性2.4 全局变量与局部变量 二、特殊形式的函数1. 子函数2. 内联函数3. 匿名函数 三、程序调试与优化1. 程序调试方法1.1 利用调试函数进行程序测试1.2 利用调试工…

【Linux】多线程概念再理解

文章目录 1. 物理内存与磁盘的关系如何理解物理内存&#xff1f;凭什么物理内存要分为一个个4KB大小&#xff1f;若以块方式存储&#xff0c;则多出的空间是否浪费&#xff1f; 2. 虚拟地址到物理地址的转换3. 缺页中断4. 为什么字符常量区是不允许被修改的&#xff1f;5. 线程…

七大软件架构设计原则详解

目录 1、概述 2、七大设计原则 2.1、开闭原则 2.2、里氏替换原则 2.3、依赖倒置原则 2.4、单一职责原则 2.5、接口隔离原则 2.6、迪米特法则 2.7、合成复用原则 3、最后 VC常用功能开发汇总&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&…