Qt创建多线程的两种方法

Qt创建多线程的两种方法

[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提供了信号与槽。
在这里插入图片描述

[4] Qt中利用多线程读取串口数据(非常重要)

原文链接:https://blog.csdn.net/guitang9913/article/details/105866552

需求:有多个串口进行数据的处理,最好需要把线程封装到串口类(serialPort)中
这样就可以在主线程中通过new多个serialport对象myPort1、myPort2等来创建多个线程管理各个串口

实现方式:继承QObject的方式来写线程, 所有东西都需要用信号和槽来进行交互!!!

思路:

主线程中需要的操作:通过控制一个按钮来操作一个串口的关闭,即控制一个线程的关闭
串口类中需要的操作:
1、不想把new QThread和myPort1->moveToThread等操作,放到主线程初始化函数中,这样程序会臃肿
所以应把上述操作放到串口类serialPort初始化中,这就需要在主程序的初始化中new一个serialport对象myPort
此时串口类serialPort初始化线程和主线程是同一个线程id

注意: 那可以把new一个serialport对象myPort这个过程放到按钮下面吗?
据大神说是不行的,逻辑有问题的,我在这里始终没有想明白。
我的想法(还没有进行验证):这样做后果是每次点击打开串口按钮都会new一个内存,但是可以再关闭掉。

2、如何开始线程,UI中开始按钮发送信号,serialport类中槽函数1-SLot_StartThread()响应这个信号,
serialport中槽函数1就是thread->start()操作来开始线程,此时这个槽函数1的线程id和主线程相同
再将thread->start()连接一个槽函数2-Slot_OpenComPort,槽函数2进行串口的初始化和打开串口,
打开串口后利用串口的readyRead()信号连接一个槽函数3-Slot_ReadSerialPortData()
此时槽函数2和槽函数3的线程号相同,但是和主线程线程id不同,开始了一个新的线程
注意:我自己在串口的多次打开和关闭试验得知,串口打开后将只需要connect一次readyRead()信号和槽函数
不然多次打开和关闭后可能会读出空字符,意思是实际发一个字符可是收到了一个字符+一个空字符
我想的是每次打开和关闭串口都会重新connect,重复了多次就会输出多的东西(有可能是我还是没有弄明白)

3、槽函数3读取接收值并发送信号给主线程,更新UI

困难:

1、这里要重点理解connect机制,也就是connect的第五个参数

借鉴一个老哥的博客:https://blog.csdn.net/kamereon/article/details/49582617
a)DirectConnection,信号发送后槽函数立即执行,由sender的所在线程执行;

b)QueuedConnection,信号发送后返回,相关槽函数由receiver所在的线程在返回到事件循环后执行;

c)默认使用的是Qt::AutoConnection,当sender和receiver在同一个线程内时,采用DirectConnection的方式,当sender和receiver在不同的线程时,采用QueuedConnection的方式。

按钮打开串口需要槽函数1立即响应,所以连接方式为DirectConnection,所以执行者是主程序,都为主线程
槽函数2和3都是通过AutoConnecTion方式连接的,和主线程不在一个线程关闭按钮信号是在主线程,想要发送信号关闭子线程,则连接方式一定为默认的方式即QueuedConnection
不然就会出现跨线程的操作。

2、线程和串口是如何关闭的?

直接关掉串口和线程,但是不能删除串口和线程。

下面开始上代码:

mySerialPort.h

#ifndef MYSERIALPORT_H
#define MYSERIALPORT_H

#include <QObject>
#include <QThread>
#include <QDebug>
#include <QtSerialPort/QSerialPortInfo>
#include <QtSerialPort/QSerialPort>
#include <QString>


class mySerialPort : public QObject
{
    Q_OBJECT
public:
    mySerialPort(QString port, int bara);

private:
    QString COM;
    int BauaRate;

    QSerialPort *myport;
    QThread *thread;
    bool isConnectFlag;

signals:
    void Sig_SendSerialPortData(QString);

public slots:
    void Slot_StartThread(); //槽函数1
    void Slot_OpenComPort(QString port, int baud_rate); //槽函数2
    void Slot_ReadSerialPortData(); //槽函数3

    void Slot_CloseThread();
};

#endif // MYSERIALPORT_H

mySerialPort.cpp

#include "myserialport.h"

#pragma execution_character_set("utf-8")

mySerialPort::mySerialPort(QString port, int bara)
{
    this->COM = port;
    this->BauaRate = bara;
    this->isConnectFlag = true;
    this->myport = new QSerialPort();
    this->thread = new QThread();

    this->moveToThread(this->thread);
    myport->moveToThread(this->thread);
    connect(thread, SIGNAL(started()), this, SLOT(Slot_OpenComPort));
    qDebug()<<tr("创建对象初始化线程为:")<<QThread::currentThread();
    thread->start();
}

void mySerialPort::Slot_StartThread()
{
    if(thread->isRunning()){
        thread->start();
        qDebug()<<tr("线程开始了,线程号:")<<QThread::currentThread();
    }
    else{
        qDebug()<<tr("线程正在运行:")<<QThread::currentThread();
    }
}

void mySerialPort::Slot_OpenComPort(QString port, int baud_rate)
{
    myport->setPortName(port);
    myport->setBaudRate(baud_rate);
    myport->setDataBits(QSerialPort::Data8);
    myport->setStopBits(QSerialPort::OneStop);
    myport->setParity(QSerialPort::NoParity);
    myport->setFlowControl(QSerialPort::NoFlowControl);

    if(myport->open(QIODevice::ReadWrite)){
        qDebug()<<tr("串口打开成功,子线程号")<<QThread::currentThread();
        if(isConnectFlag){
            connect(myport, SIGNAL(readyRead()), this, SLOT(Slot_ReadSerialPortData()));
            isConnectFlag = false;
        }
    }
    else{
        qDebug()<<tr("串口打开失败");
    }

}

void mySerialPort::Slot_ReadSerialPortData()
{
    qDebug()<<tr("读数据,子线程号:")<<QThread::currentThread();
    QByteArray buf = myport->readAll();
    if(!buf.isEmpty()){
        QString str = buf;
        qDebug()<<str;
        emit Sig_SendSerialPortData(str);
    }
    buf.clear();
}

void mySerialPort::Slot_CloseThread()
{
    qDebug()<<tr("析构函数执行线程:")<<QThread::currentThread();
    //关闭串口
    myport->clear();
    myport->close();
    //myPort->deleteLater();

    //杀死线程
    if(thread->isRunning())
    {
        thread->quit();
        thread->wait();
        //thread->deleteLater();
    }
}

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include QMainWindow>
#include "myserialport.h"

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_pBtn_openPort_clicked();
    void slot_ReadSerialPortData(QString data);

signals:
    void sig_OpenComPort(QString port, int baud_rate);
void sig_StartThread();
void sig_CloseThread();

private:
    Ui::MainWindow *ui;
    mySerialPort *mySerialPort1;
};
#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"

#pragma execution_character_set("utf-8")

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    mySerialPort1 = new mySerialPort("COM5", 115200);
    connect(mySerialPort1, &mySerialPort::Sig_SendSerialPortData, this, &MainWindow::slot_ReadSerialPortData);
    connect(this, &MainWindow::sig_OpenComPort, mySerialPort1, &mySerialPort::Slot_OpenComPort);
    connect(this, &MainWindow::sig_StartThread, mySerialPort1, &mySerialPort::Slot_StartThread);
    connect(this, &MainWindow::sig_CloseThread, mySerialPort1, &mySerialPort::Slot_CloseThread);

    QStringList baudrate;
    baudrate<<"9600"<<"19200"<<"38400"<<"57600"<<"115200";
    ui->comboBox_baudrate->addItems(baudrate);
    /*查找电脑当前可用串口*/
    /*返回当前存在的串口链表*/
    QList<QSerialPortInfo> m_portList;
    m_portList = QSerialPortInfo::availablePorts();

    // 遍历每个可用的串口
    for (int i = 0; i < m_portList.count(); i++)
    {
        ui->comboBox_comPort->addItem(QString(m_portList[i].portName()));
    }
    qDebug()<<"MainWindow初始化线程为:"<<QThread::currentThread();

}

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


void MainWindow::on_pBtn_openPort_clicked()
{
    QString portname;
    int baudrate;
    static bool btnFlag = true;
    if(btnFlag == true){
        ui->pBtn_openPort->setText(tr("关闭串口"));
        portname = ui->comboBox_comPort->currentText();
        baudrate = ui->comboBox_baudrate->currentText().toInt();
        ui->comboBox_comPort->setEnabled(false);
        ui->comboBox_baudrate->setEnabled(false);
        btnFlag = false;
        //emit sig_StartThread();
        emit sig_OpenComPort(portname, baudrate);
    }
    else{
        ui->pBtn_openPort->setText(tr("打开串口"));
        ui->comboBox_comPort->setEnabled(true);
        ui->comboBox_baudrate->setEnabled(true);
        btnFlag = true;
    }
}

void MainWindow::slot_ReadSerialPortData(QString data)
{
    static int recv_cnt = 0;
    ui->textEdit_RecvData->append(data);
    recv_cnt++;
//    if(recv_cnt > 100){
//        ui->textEdit_RecvData->clear();
//        recv_cnt = 0;
//    }
    ui->lineEdit_cnt->setText(QString::number(recv_cnt*7));
}

运行效果:

在这里插入图片描述

[5] QTcpServer运行在子线程

https://blog.csdn.net/m0_46577050/article/details/124830613?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168540811016800192255970%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=168540811016800192255970&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_ecpm_v1~rank_v31_ecpm-4-124830613-null-null.268v1control&utm_term=%E5%A4%9A%E7%BA%BF%E7%A8%8B&spm=1018.2226.3001.4450

[6] QSerialPort、QTcpSocket等QIODevice设备的跨线程调用报错解决办法以及多线程的使用

https://blog.csdn.net/m0_46577050/article/details/124789985?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168540811016800192255970%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=168540811016800192255970&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_ecpm_v1~rank_v31_ecpm-1-124789985-null-null.268v1control&utm_term=%E5%A4%9A%E7%BA%BF%E7%A8%8B&spm=1018.2226.3001.4450

[7] 利用Qt多线程机制实现双路串口数据流的接收和发送(附工程源代码)

原文链接:https://blog.csdn.net/SmartTiger_CSL/article/details/104383717

在上一篇文章的基础上,编写了一个对话框程序,可同时收发两路串口数据,每一路串口均在独立的子线程中实现。增加了清空edit的按钮。

1 主程序界面

在这里插入图片描述

2 两个子线程的线程号(调试信息中输出)

在这里插入图片描述
主线程的ID号为0x179c,两个串口子线程类的构造均是在主线程中,串口的启动、接收数据均在各自的子线程中,子线程ID号分别在0x14e4和0x5b0。而串口的关闭是在主线程中。这是和connect的配置有关。代码如下:

#include "serialcontroller.h"
#include <QDebug>

SerialController::SerialController(QObject *parent) : QObject(parent)
{
    m_portId = -1;
}

SerialController::~SerialController()
{
    if(m_serialThread.isRunning())
    {
        m_serialPort->closePort();
        delete m_serialPort;
        m_serialThread.quit();
        m_serialThread.wait();
     }

}
void SerialController::initCtrl(int portId,QString portName,long portBaud)
{
    m_portId = portId;
    m_portName = portName;
    m_portBaud = portBaud;
    qDebug()<<"Controller is running in main thread: "<<QThread::currentThreadId();

    //实例对象保存在堆上,没有父对象的指针要想正常销毁,需要将线程的 finished() 信号关联到 QObject 的 deleteLater() 让其在正确的时机被销毁

    m_serialPort = new SerialPort(m_portId,m_portName,m_portBaud);
    //m_serialPort对象不能有父对象。
    m_serialPort->moveToThread(&m_serialThread);

    connect(this,&SerialController::startRunning,m_serialPort,&SerialPort::startPort);
    connect(&m_serialThread,&QThread::finished,m_serialPort,&QObject::deleteLater);
    connect(this,&SerialController::ctrlSendData,m_serialPort,&SerialPort::write_Data);//从主线程发来的数据写入串口
    connect(m_serialPort,&SerialPort::receive_data,this,&SerialController::ctrlReceiveData);//从串口读取的数据发送给主线程
}
void SerialController::startCtrl()
{
    m_serialThread.start();
    emit startRunning();

}
void SerialController::stopCtrl()
{
    if(m_serialThread.isRunning())
    {       
        m_serialPort->closePort();
        m_serialThread.quit();//会自动发送finished信号
        m_serialThread.wait();       
    }
}

串口的启动和接收数据所在的函数startPort与QThread类的connect信号关联,因此是在子线程中执行;而串口的关闭函数closePort没有与connect关联,不是槽函数,是在SerialController类的stopCtrl中执行,SerialController存在于主线中,因此closePort在主线程中执行。

因此,这里验证里上一篇文章中的重点(3):
(3)controller、worker 对象到底在哪个线程?「在哪创建就属于哪」这句话放在任何地方都是适用的。而 moveToThread() 函数的作用是将槽函数在指定的线程中被调用。也就是说controller、worker对象均在主线程中。除了绑定在connect上的槽函数(及槽函数体调用的函数)外,worker的其余函数也在主线程中执行。这个connect发送者可以是SerialController本身(this),也可以是m_serialThread
moveToThread()并不是将整个worker 对象“搬移”到controller线程中,而是将connect中的槽函数放到controller线程中执行。不注意这一点的话,很可能出现“QObject::Cannot create children for a parent that is in a different thread”问题。或者出现“耗时工作代码”仍在主线程中运行的情况。

备注: 该工程的源代码又增加了保存原始数据流的功能,但是保存数据功能还不完善,将源代码上传,供大家学习参考:
https://download.csdn.net/download/SmartTiger_CSL/13094870
如果没有积分的小伙伴可以去百度网盘下载
https://pan.baidu.com/s/1o0K4-NQ7DuMwYzxX_rRJPg
提取码:dx6d

在这里插入图片描述

[8] Qt 实现多线程的串口通信

https://blog.csdn.net/qq_45662588/article/details/117750982?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-0-117750982-blog-125774511.235v36pc_relevant_anti_vip&spm=1001.2101.3001.4242.1&utm_relevant_index=1

简述
Qt下无论是RS232、RS422、RS485的串口通信都可以使用统一的编码实现。本文把每路串口的通信各放在一个线程中,使用movetoThread的方式实现。

代码之路
用SerialPort类实现串口功能,Widget类调用串口。

serialport.h如下

#include <QObject>
#include <QSerialPort>
#include <QString>
#include <QByteArray>
#include <QObject>
#include <QDebug>
#include <QObject>
#include <QThread>

class SerialPort : public QObject
{
  Q_OBJECT
public:
  explicit SerialPort(QObject *parent = NULL);
  ~SerialPort();

  void init_port();  //初始化串口

public slots:
  void handle_data();  //处理接收到的数据
  void write_data();     //发送数据

signals:
  //接收数据
  void receive_data(QByteArray tmp);

private:
  QThread *my_thread;
  QSerialPort *port;
};

serailport.cpp如下

#include "serialport.h"

SerialPort::SerialPort(QObject *parent) : QObject(parent)
{
    my_thread = new QThread();

    port = new QSerialPort();
    init_port();
    this->moveToThread(my_thread);
    port->moveToThread(my_thread);
    my_thread->start();  //启动线程

}

SerialPort::~SerialPort()
{
    port->close();
    port->deleteLater();
    my_thread->quit();
    my_thread->wait();
    my_thread->deleteLater();
}

void SerialPort::init_port()
{
    port->setPortName("/dev/ttyS1");                   //串口名 windows下写作COM1
    port->setBaudRate(38400);                           //波特率
    port->setDataBits(QSerialPort::Data8);             //数据位
    port->setStopBits(QSerialPort::OneStop);           //停止位
    port->setParity(QSerialPort::NoParity);            //奇偶校验
    port->setFlowControl(QSerialPort::NoFlowControl);  //流控制
    if (port->open(QIODevice::ReadWrite))
    {
        qDebug() << "Port have been opened";
    }
    else
    {
        qDebug() << "open it failed";
    }
    connect(port, SIGNAL(readyRead()), this, SLOT(handle_data()), Qt::QueuedConnection); //Qt::DirectConnection
}

void SerialPort::handle_data()
{
    QByteArray data = port->readAll();
    qDebug() << QStringLiteral("data received(收到的数据):") << data;
    qDebug() << "handing thread is:" << QThread::currentThreadId();
    emit receive_data(data);
}

void SerialPort::write_data()
{
    qDebug() << "write_id is:" << QThread::currentThreadId();
    port->write("data", 4);   //发送“data”字符
}

widget.h的调用代码

#include "serialport.h"
public slots:
void on_receive(QByteArray tmpdata);
private:
SerialPort *local_serial;

widget.cpp调用代码

//构造函数中
local_serial = new SerialPort();
connect(ui->pushButton, SIGNAL(clicked()), local_serial, SLOT(write_data()),Qt::QueuedConnection);
connect(local_serial, SIGNAL(receive_data(QByteArray)), this, SLOT(on_receive(QByteArray)), Qt::QueuedConnection);
//on_receive槽函数
void Widget::on_receive(QByteArray tmpdata)
{
 ui->textEdit->append(tmpdata);
}

[9] QT串口单独线程高速接收

原文链接:https://blog.csdn.net/u012123768/article/details/128062292

之前做了个串口程序,利用QT程序自带的readReady信号来读取接收到的串口信息。给别人使用的时候,发现了一个问题:就是对方串口发送速度太快,程序一运行就自动退出。可能原因分析:readReady无法快速响应。对方速率是约是200Hz,每一包数据为23个字节。将接收到的数据打印出来,一下读取的数据太多,其中我有个操作是放进QString中,因为readReady产生速度慢,缓存区就会缓存很多数据,下一次读取过多,超了Qstring的长度,程序就自动退出了。

后来,我想使用一个单独线程在那不停读取,然后将读取的数据通过信号发送出去。于是我就利用原始的QserialPort修改了myserialport类。

myserialport.h

#ifndef MYSERIALPORT_H
#define MYSERIALPORT_H
 
#include <QThread>
#include <QDebug>
#include <QSerialPort>
#include <QEventLoop>
#include <QTimer>
#include <QDateTime>            //===时间
 
class MySerialPort:public QThread
{
    Q_OBJECT
public:
    MySerialPort();
    QSerialPort *serialport;
public:
    void setPortName(QString portname);
    bool open(QIODevice::OpenMode mode);
    bool setBaudRate(qint32 baudrate);
    bool setParity(QSerialPort::Parity parity);
    bool setDataBits(QSerialPort::DataBits databits);
    bool setFlowControl(QSerialPort::FlowControl flowcontrol) ;
    bool setStopBits(QSerialPort::StopBits stopbits);
    bool setDataTerminalReady(bool ready);
    void setStopFlag(bool flag);
    bool mySerialPortIsOpen();
private:
    bool stopFlag;
signals:
    void uartSendData(QByteArray data);
private:
    void run() override;
};
 
#endif // MYSERIALPORT_H

myserialport.cpp

#include "myserialport.h"
 
MySerialPort::MySerialPort()
{
    serialport = new QSerialPort();
    stopFlag = false;
}
 
void MySerialPort::run()
{
    //qDebug()<<"parity:"<<serialport->parity();
    while(!stopFlag)
    {
        //===接收数据
        QByteArray buffer = serialport->readAll();
        if(buffer.size()>0)
        {
            //qDebug()<<buffer.toHex()<<endl;
            emit uartSendData(buffer);
        }
 
        //===事件循环
        QEventLoop loop;    // 非常重要, 没有事件循环数据无法发送.如果读取的话,不加一些等待,读取的数据都是空
        QTimer::singleShot(5, &loop, SLOT(quit()));
        loop.exec();
 
        //===测试发送数据
        /*QByteArray dataToSend;
        dataToSend.resize(23);
        dataToSend[0]=0xCA;
        dataToSend[1]=0xAA;
        serialport->write(dataToSend);
        QDateTime current_datetime = QDateTime::currentDateTime();*/
        //qDebug()<<current_datetime.toString("yyyy-MM-dd hh:mm:ss.zzz")<<endl;
    }
    serialport->close();
}
 
void MySerialPort::setStopFlag(bool flag)
{
    stopFlag = flag;
}
 
void MySerialPort::setPortName(QString portname)
{
    serialport->setPortName(portname);
}
bool MySerialPort::open(QIODevice::OpenMode iodevice)
{
    if(serialport->open(iodevice))
    {
        return true;
    }
    return false;
}
bool MySerialPort::setBaudRate(qint32 baudrate)
{
    if(serialport->setBaudRate(baudrate))
    {
        return true;
    }
    return false;
}
bool MySerialPort::setParity(QSerialPort::Parity parity)
{
    if(serialport->setParity(parity))
    {
        return true;
    }
    return false;
}
bool MySerialPort::setDataBits(QSerialPort::DataBits databits)
{
    if(serialport->setDataBits(databits))
    {
        return true;
    }
    return false;
}
bool MySerialPort::setFlowControl(QSerialPort::FlowControl flowcontrol)
{
    if(serialport->setFlowControl(flowcontrol))
    {
        return true;
    }
    return false;
}
bool MySerialPort::setStopBits(QSerialPort::StopBits stopbits)
{
    if(serialport->setStopBits(stopbits))
    {
        return true;
    }
    return false;
}
 
bool MySerialPort::mySerialPortIsOpen()
{
    return serialport->isOpen();
}
 
bool MySerialPort::setDataTerminalReady(bool ready)
{
    if(serialport->setDataTerminalReady(ready))
    {
        return true;
    }
    return false;
}

调用程序片段

void readWindow::on_uartPushButton_clicked()
{
    if(fileName.isNull())
    {
        QMessageBox::critical(this,"(╯︵╰)","对不起,我尝试读取协议文件,但是我好像迷路了,没有找到协议文件");
        return;
    }
 
    if(!fileTextSave.isOpen())
    {
        QMessageBox::critical(this,"(╯︵╰)","实在抱歉,没有创建新的实验记录,我无法区分每个实验的不同区别");
        return;
    }
 
    if(ui->uartPushButton->text() == "打开串口")
    {
 
        QDateTime current_datetime = QDateTime::currentDateTime();
        ui->operationTextBrowser->insertPlainText(current_datetime.toString("yyyy-MM-dd hh:mm:ss.zzz")+":");
        ui->operationTextBrowser->insertPlainText("打开串口\n");
 
        //===获取串口设置信息
        QString comStr;
        comStr = ui->comComboBox->currentText();
 
        //===串口初始化
        m_myserialport->setPortName(comStr);
        if(m_myserialport->open(QIODevice::ReadWrite))
        {
            //===设置波特率
            m_myserialport->setBaudRate(ui->rateComboBox->currentText().toInt());
 
            //===设置校验位
            switch(ui->checkComboBox->currentIndex())
            {
            case 0:
                m_myserialport->setParity(QSerialPort::NoParity);
                break;
            case 1:
                m_myserialport->setParity(QSerialPort::OddParity);
                break;
            case 2:
                m_myserialport->setParity(QSerialPort::EvenParity);
                break;
            default:
                break;
            }
 
            //===设置数据位
            switch(ui->dataComboBox->currentIndex())
            {
            case 0:
                m_myserialport->setDataBits(QSerialPort::Data5);
                break;
            case 1:
                m_myserialport->setDataBits(QSerialPort::Data6);
                break;
            case 2:
                m_myserialport->setDataBits(QSerialPort::Data7);
                break;
            case 3:
                m_myserialport->setDataBits(QSerialPort::Data8);
                break;
            default:
                break;
            }
 
            //===设置流控制
            m_myserialport->setFlowControl(QSerialPort::NoFlowControl);
 
            //===设置停止位
            switch(ui->stopComboBox->currentIndex())
            {
            case 0:
                m_myserialport->setStopBits(QSerialPort::OneStop);
                break;
            case 1:
                m_myserialport->setStopBits(QSerialPort::OneAndHalfStop);
                break;
            case 2:
                m_myserialport->setStopBits(QSerialPort::TwoStop);
                break;
            default:
                break;
            }
            m_myserialport->setDataTerminalReady(false);//===关闭readyRead信号
            connect(m_myserialport,SIGNAL(uartSendData(QByteArray)),this,SLOT(uartReadData(QByteArray)));
            m_myserialport->start();
            //m_myserialport->setDataTerminalReady(true);//===设置“管脚控制状态”产生readyRead()信号
            qDebug()<<"连接成功"<<endl;
 
            ui->operationStatusBar->showMessage("串口打开成功!");//===状态栏显示操作信息
            ui->uartPushButton->setText("关闭串口");
            //connect(m_serialport,SIGNAL(readyRead()),this,SLOT(uartReadData()));
 
            //===开启线程
            if(databaseSaveFlag)
            {
                wbThread = new writeDatabaseThread();
                wbThread->setQuery(query);
                wbThread->setStopFlag(false);
                wbThread->start();
                connect(this,SIGNAL(sendCommand(QString)),wbThread,SLOT(getQueryCommand(QString)));
            }
        }
        else
        {
            QMessageBox::critical(this,"(╯︵╰)","对不起,我无法打开这个"+comStr+"串口,不知道发生了什么");
        }
    }
    else if(ui->uartPushButton->text() == "关闭串口")
    {
 
        QDateTime current_datetime = QDateTime::currentDateTime();
        ui->operationTextBrowser->insertPlainText(current_datetime.toString("yyyy-MM-dd hh:mm:ss.zzz")+":");
        ui->operationTextBrowser->insertPlainText("关闭串口\n");
 
        if(databaseSaveFlag)
        {
            wbThread->setStopFlag(true);
            wbThread->quit();
        }
        if(fileTextSave.isOpen())
        {
            fileTextSave.close();
        }
        m_myserialport->setStopFlag(true);
        //m_serialport->close();
        ui->operationStatusBar->showMessage("关闭串口!");//===状态栏显示操作信息
        ui->uartPushButton->setText("打开串口");
    }
}

出现问题:修改完了之后,我进行测试,发现readAll函数一直读不出来东西,一直不知道什么原因。测试发送数据,发送数据是正常的,说明串口设置等操作都是正确的,那么问题在哪儿呢?后来不停的搜,发现有的程序加了事件循环:说了读太快,内容是读不出来了。

解决方式:我就在程序里加了个4ms的事件循环,因为他们需要的200Hz的,我就读的更快一点。

修改后整体程序测试:利用自己的程序,按照5ms的事件循环去发送数据,然后按照4ms的事件循环进行读取,发现是可以正常读取的,漏帧情况很少。

下图为发送端情况,发送的时间间隔大致为5-6ms。
在这里插入图片描述

[10] 简单说说对QT中moveToThread实现多线程操作的理解(非常重要)

原文链接:https://blog.csdn.net/yzt629/article/details/105429367

在平时的编码过程中经常碰到QT的多线程问题,也大量接触了QT中的两种主流多线程写法,一种是继承QThread类并重载run函数,在run函数中写一个状态机或者计时器来实现对线程运作;一种是通过moveToThread的方式实现事件托管从而实现线程运作。前者虽然是传统写法但是弊病较多,这里主要针对后者来进行说明与理解。

1.常见情况分析:该方法在进行代码编写时的常见情况如下:

class tempActive
{
public:
	tempActive()
    {
    	...
    	a = new A;
    	m_thread = new QThread;
    	a->moveToThread(m_thread);
    	...
    ]
private:
	A* a;
	QThread* m_thread;
    ...
};

上述代码中,tempActive为某一工作相关类,A为某一自定义类或预定义类,其实例a与实例m_thread往往在tempActive的构造函数中实现,并在其中将a实例moveToThread至m_thread线程之中。
这里需要着重理解一点,并非是将a实例相关的所有的工作“移动”到了m_thread线程,而是将所有a实例相关的事件托管到m_thread线程执行。换句话说,就是通过信号槽connect或者invokeMethod触发a实例中槽函数产生的事件,将会被放置到m_thread线程中执行,从而实现了多线程工作。也就是说,此时通过a->show()等方式调用直接调用a实例中的函数,无法实现多线程功能。

2.实验验证:

为了验证以及加强理解上述结论,在这里做一个简单的实验如下:

class A : public QObject
{
    Q_OBJECT
public:

public slots:
    void show()
    {
        qDebug() << "show:" << QThread::currentThreadId();
    }
};

class MainWindow : public QObject
{
    Q_OBJECT

public:
    explicit MainWindow(QObject *parent = 0);
    ~MainWindow();
public slots:
    void mainShow() {qDebug() << "mainShow:" << QThread::currentThreadId();}
signals:
    void s_test();
private:
    A* a;
    QTimer* m_timer;
    QThread* m_thread;
};

MainWindow::MainWindow(QObject *parent) :
    QObject(parent)
{
    a = new A;
    m_thread = new QThread;
    m_timer = new QTimer;
    m_timer->setInterval(500);
    m_thread->start();

    qDebug() << "A before move:" << QThread::currentThreadId();	//A before move: 0x2a0c
    a->show();	//show: 0x2a0c
    QMetaObject::invokeMethod(a,"show",Qt::DirectConnection);	//show: 0x2a0c
    QObject::connect(this,SIGNAL(s_test()),a,SLOT(show()),Qt::DirectConnection);
    emit s_test();	//show: 0x2a0c
    QObject::disconnect(this,SIGNAL(s_test()),a,SLOT(show()));

    a->moveToThread(m_thread);
    qDebug() << "A after move:" << QThread::currentThreadId();	//A after move: 0x2a0c
    a->show();	//show: 0x2a0c
    QMetaObject::invokeMethod(a,"show",Qt::BlockingQueuedConnection);	//show: 0x3a9c
    QObject::connect(this,SIGNAL(s_test()),a,SLOT(show()),Qt::BlockingQueuedConnection);
    emit s_test();	//show: 0x3a9c
    QObject::disconnect(this,SIGNAL(s_test()),a,SLOT(show()));

    qDebug() << "m_timer before move:" << QThread::currentThreadId();	//m_timer before move: 0x2a0c
    m_timer->moveToThread(m_thread);
    this->moveToThread(m_thread);
    qDebug() << "m_timer after move:" << QThread::currentThreadId();	//m_timer after move: 0x2a0c
    QObject::connect(m_timer,SIGNAL(timeout()),this,SLOT(mainShow()),Qt::DirectConnection);	
    QMetaObject::invokeMethod(m_timer,"start",Qt::BlockingQueuedConnection);	//mainShow: 0x3a9c
}

在上述函数中,通过MainWindow的构造函数进行实验,这里分步分析下:

当实例a还未移动至m_thread之前,其所有的相关操作均处于当其被new时所处的线程,由于MainWindow在主线程中被构造,所以此时实例a位于主线程0x2a0c。此时通过任何方式,无论直接调用a_>show(),抑或是通过信号槽和invokeMethod调用show(),最终触发时show()必然在主线程0x2a0c之中。
当实例a被移动至m_thread之后,其相关事件被托管,但是该实例本身仍位于主线程0x2a0c中,但是通过信号槽和invokeMethod调用show()时发生在m_thread线程0x3a9c之中。
当m_timer与this被移动至m_thread之后,其所处线程仍不变为主线程0x2a0c,此时m_timer与this为同一线程,所以信号槽连接时使用DirectConnection,并且需要通过invokeMethod方式来调用m_timer的start方可在主线程0x2a0c中调用m_thread中的m_timer.start()函数(不然会报QTimer无法在另外线程打开的错误,这其实是QT对于QTimer机制的一种保护,有兴趣的读者可以研究下这里将QTimer进行moveToThread之后对于线程的限制保护机制),最后执行的mainShow()函数也确确实实的都在m_thread的线程0x3a9c中执行。

3.结论总结

moveToThread方式并非“无脑”移动,是一种事件的托管;
事件的产生只能通过信号槽或invokeMethod方式来实现,其他的方式(如直接调用)将会导致所调用函数在进行直接调用的线程之中执行,而非moveToThread之后的线程。
由于QTimer操作对于线程的限制性,需要保证执行QTimer的相关函数时,如果想采用直接调用则需保证调用函数线程与QTimer所处线程一致,否则就必须使用信号槽或invokeMethod的方式来调用QTimer相关的函数。
“QT官方文档moveToThread的Warning”:moveToThread往往只能将当前对象从其当前所处线程“推”到某一线程中去(比如1中的常见情况,即在构造函数中将实例a从构建tempActive的线程“推”到了m_thread线程中),不能将当前对象从某线程中“拉”到该线程(即无法在实例a不处在的线程中执行对实例a的moveToThread操作,因为无法在另一线程控制a的自身线程)

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值