Qt中的多线程

1 线程的基本概念

通俗地来说,线程是进程中实际执行代码的最小单元,它由操作系统安排调度(何时启动、何时运行和暂停以及何时消亡)。在一个进程中,线程是实际干活的单位。因此一个进程至少得有一个线程,我们把这个线程称之为“主线程”。

在Qt中,如果管理线程的线程对象被销毁时该线程仍在运行,则程序将会报告异常。所以在Qt程序中,如果退出主线程时仍有子线程在运行,程序将会报告异常。除非管理这些子线程的对象在程序退出时不会被销毁。例如:

// 正常退出的程序
int main(int argc, char *argv[])
{
    SubThread *subThread = new SubThread;
    subThread->start();
    return 0;
}

// 退出异常的程序
int main(int argc, char *argv[])
{
    SubThread subThread;
    subThread.start();
    return 0;
}

退出异常提示如下图所示:
QThread退出异常提示
第一段程序虽然正常退出了,但是发生了内存泄漏。第二段程序则是直接在退出时发生了异常。所以在使用多线程时,一定要确保线程的正常退出。

2 线程的创建、启动和退出

在Qt中,线程的创建和使用是依赖于QThread这个类。QThread提供了一种依赖于操作系统的线程管理方式。在程序中,每个QThread对象只管理一个线程,而每个线程都是从QThread类提供的run()函数开始执行。

2.1 QThread类创建线程的两种做法

2.1.1 继承QThread。

通过继承QThread类来重写QThread的run()函数。从而在run()函数中实现我们需要完成的功能。(QThread自身的run()函数是protected且virtual的)。如:

class SubThread : public QThread
{
    Q_OBJECT
public:
    explicit SubThread(QObject *parent = nullptr);

signals:

    // QThread interface
protected:
    void run() Q_DECL_OVERRIDE;
};

void SubThread::run()
{
    qDebug() << "sub thread start";
    qDebug() << "sub thread id=" << QThread::currentThreadId();

    int i = 0;
    while (i <= 1000)
    {
        if (i % 100 == 0)
        {
            qDebug() << i << " ***  sub thread is running....";
        }
        i++;
    }
    qDebug() << "sub thread exit";
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    qDebug() << "main thread id = " << QThread::currentThreadId();
    SubThread sub;
    sub.start();
    return a.exec();
}

上述程序的运行结果为:
在这里插入图片描述

2.1.2 使用moveToThread()方法将一个QObject对象移动到一个QThread对象中

这种方法用到两个对象。一个是实现功能的worker对象,一个是提供线程能力的QThread对象。步骤如下:

  • 第一步:创建worker。如:

    // 实现功能的worker
    class ThreadWorker : public QObject
    {
        Q_OBJECT
    public:
        explicit ThreadWorker(QObject *parent = nullptr);
    
        void doWork(); // 作为线程处理函数
        void exit();
    
    private:
        bool m_exit;
    
    };
    
    void ThreadWorker::doWork()
    {
        qDebug() << "worker start";
        qDebug() << "sub thread id=" << QThread::currentThreadId();
        m_exit = false;
        int i = 0;
        while (!m_exit && i <= 1000)
        {
            if (i % 100 == 0)
            {
                qDebug() << i << " ***  worker is running....";
            }
            i++;
        }
        qDebug() << "worker exit";
    }
    
    void ThreadWorker::exit()
    {
        m_exit = true;
    }
    
  • 第二步:将worker移入另外的线程管理对象中。

        QThread *subThread = new QThread(this);
        m_worker = new ThreadWorker;
        m_worker->moveToThread(subThread);
    
  • 第三步:启动线程。这里启动线程,不仅要调用QThread的start()方法,还需要调用worker的线程处理函数。上例中即是doWork()函数。对于start()调用,则是在调用线程中直接执行subThread.start()即可,而对于doWork()的调用,则是必须通过信号与槽方式来执行。这是为了确保对doWork()的调用是在不同的线程中。这里分为两种情形:
    (1). 先调用了start(),然后根据用户操作和程序执行情况触发子线程处理信号,然后在该信号对应的槽中调用doWork()。如:

        Widget::Widget(QWidget *parent)
    	    : QWidget(parent)
    	{
    	    qDebug() << QThread::currentThreadId();
    	    QThread *subThread = new QThread(this);
    	    m_worker = new ThreadWorker;
    	    m_worker->moveToThread(subThread);
    	    QPushButton *startBtn = new QPushButton(this);
    	    startBtn->setText("start");
    	    startBtn->setGeometry(0,0, 100, 50);
    	    connect(startBtn, &QPushButton::clicked, this, &Widget::startWorker);
    	    connect(this, &Widget::workerStart, m_worker, &ThreadWorker::doWork);
    	    subThread->start();
    	}
    	
    	void Widget::startWorker()
    	{
    	    emit workerStart();
    	}
    

    这里,doWork()是作为workerStart信号的槽被调用的。this是当前线程的对象,而m_worker是subThread中的对象。这两个对象处于不同的线程中,所以这个connect的连接方式是Qt::QueuedConnection。而对于Qt::QueuedConnection连接方式,槽函数是执行在接收信号的对象所在的线程中的。所以,doWork()是执行在其他线程中。
    (2). 在用户操作和程序执行情况触发子线程处理信号时,调用start(),然后在QThread的started信号的槽函数中调用doWork()。如:

    Widget::Widget(QWidget *parent)
        : QWidget(parent)
    {
        qDebug() << QThread::currentThreadId();
        m_thread = new QThread(this);
        m_worker = new ThreadWorker;
        m_worker->moveToThread(m_thread);
        QPushButton *startBtn = new QPushButton(this);
        startBtn->setText("start");
        startBtn->setGeometry(0,0, 100, 50);
        connect(startBtn, &QPushButton::clicked, this, &Widget::startWorker);
        QObject::connect(m_thread, &QThread::started, [=](){
              qDebug() << QThread::currentThreadId();
            m_worker->doWork();
        });
    }
    void Widget::startWorker()
    {
        m_thread->start();
    }
    

    这里是通过匿名槽函数来调用doWork()的。而对于槽函数是匿名函数的connect,其连接方式为Qt::DirectConnection,其槽函数执行在发出信号的线程中。所以,这里的doWork()也是执行在其他线程中。

    针对上面两种情形,(1)中在线程处理函数被调用之前就启动了线程,这可能会导致资源上的一些浪费;而(2)中在每次操作时去启动线程,则在每次启动时必须先检查线程的状态,这无疑会增加逻辑控制的复杂度。

在上例中moveToThread()的参数除了是QThread对象外,也可以是QThread的子类对象。如上面的SubThread的对象。如果使用的是QThread的子类对象,则需要注意的是QThread的run()方法中默认包含一个event loop。所以,使用的子类对象的run()中也需要提供一个event loop。否则线程会提前退出从而导致doWork()函数无法执行。在上面所述的(1)情形中,由于先调用的start(),所以run()会优先执行,如果没有event loop,则在run()执行完成后线程就将退出了,所以在触发doWork时,其将不会执行。而在(2)情形中,doWork()会优先于run()执行,所以对于(2)中的调用,即使run()中没有event loop,doWork()也会执行一次。但是如果doWork中没有阻塞处理,doWork就将只能运行一次,因为doWork已经返回,这时再启动线程也就毫无意义了。如果doWork中有阻塞,则线程的start()函数可能都无法北调用,因为程序发生了阻塞,所以这也毫无意义。因此,线程的start()一定要早于doWork被调用。

2.2 线程退出

正如前面所述,如果线程未正常退出,则在销毁线程管理对象时将会发生异常。而要使线程退出,一种方法是使QThread的run()函数正常返回;另一种方法是强制终止线程。QThread类提供了terminate()函数来强制终止一个线程。不过该方法的最终执行依赖于系统的时间调度,所以其行为是不可控的。而且使用此函数时,线程不能清理其之前创建的一些资源,所以并不推荐使用这种方法。通常是使用QThread提供的quit()和wait()函数来使一个线程退出。

  • quit():quit是使run()函数中的event loop退出。比如,run()中含有一个名为threadLoop的event loop,则quit的作用相当于threadLoop.exit()。所以,如果run()函数中没有event loop,则quit()函数并不起什么作用;而如果run()函数含有其他循环处理,在要是run()函数返回,则必须先要退出这个循环处理。如上面SubThread中的run()函数,其中并没有event loop,所以要使其退出,只能等待其执行结束。如果循环处理时间比较长,则可以通过设置标识来控制循环退出。上面ThreadWorker中的m_exit,这就是一个控制循环退出的标识。通过ThreadWorker使用线程的退出示例如下:
    void Widget::stopThread()
    {
        m_worker->exit(); // 使线程处理函数返回
        m_thread->quit(); // 使QThread的run()函数返回
        m_thread->wait(1000); // 等待线程处理完成
    }
    
  • wait():使程序阻塞指定的时间。由于quit()函数是非阻塞的,而线程清理一些资源是需要花费一些时间的。所以通常会在调用quit()后调用wait()来使程序等待一段时间。但是这不能作为线程结束的可靠依据。线程结束还是需要通过QThread的finished信号来处理。

3 线程同步(安全)

当一个变量或对象需要在不同的线程之间被使用时,则就可能存在同步问题。如:

void SubThread::run()
{
   
    qDebug() << "sub thread start";
    qDebug() << "sub thread id=" << QThread::currentThreadId();
    m_exit = false;
    int i = 0;
    while (!m_exit && i <= 100000)
    {
   
        if (i % 100 == 0)
        
  • 3
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Qt Creator 多线程读取文件到程序显示 利用QT Creator多任务读取一个文档到程序里 为了防止直接读取文件里的内容太大而发生卡顿,于是多线程读取将更高效的解决这个问题。 效果图如下: 其pro文件无需改动,默认就好,头文件h里面的内容为 #ifndef MAINWINDOW_H #define MAINWINDOW_H #include #include #include QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MyObj; class MyObj : public QObject { Q_OBJECT public: MyObj(); //新的线程 signals: void toLine(QString line); private slots: void doWork(); }; class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); private slots: void appendText(QString); //定义一个槽 private: Ui::MainWindow *ui; QThread *t; MyObj *obj; }; #endif // MAINWINDOW_H 而MAIN主文件的内容为了防止文乱码做了如下修改: #include "mainwindow.h" #include #include int main(int argc, char *argv[]) { QApplication a(argc, argv); //设置文字体 防止乱码 a.setFont(QFont("Microsoft Yahei", 9)); //设置文编码 #if (QT_VERSION <= QT_VERSION_CHECK(5,0,0)) #if _MSC_VER QTextCodec *codec = QTextCodec::codecForName("GBK"); #else QTextCodec *codec = QTextCodec::codecForName("UTF-8"); #endif QTextCodec::setCodecForLocale(codec); QTextCodec::setCodecForCStrings(codec); QTextCodec::setCodecForTr(codec); #else QTextCodec *codec = QTextCodec::codecForName("UTF-8"); QTextCodec::setCodecForLocale(codec); #endif MainWindow w; w.show(); return a.exec(); } 接下来重点来了,源文件CPP里为 #include "mainwindow.h" #include "ui_mainwindow.h" #include #include #include #include #include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); t = new QThread(); //QThread obj = new MyObj(); obj->moveToThread(t); qDebug()<<"main thread:"<<QThread::currentThread(); connect(t,SIGNAL(started()), obj, SLOT(doWork())); connect(obj,SIGNAL
Qt实现多线程数据收发的方法如下: 首先,在头文件"datareceiver.h"定义了一个名为DataReceiver的类,继承自QThread类。该类包含了一个静态的数据池DataPool,用于存储接收到的数据。同时,该类还定义了一个单例模式的getInstance()函数,用于获取DataReceiver的实例。在构造函数,可以传入一个QObject类型的父对象。还有一个stop()函数,用于停止线程的执行。在run()函数,通过循环判断数据池是否为空,如果不为空,则取出第一个数据并发出信号oneDataReady()。 在源文件"datareceiver.cpp",首先定义了一个静态的QStringList类型的数据池list,并初始化为空列表。然后使用Q_GLOBAL_STATIC宏定义了一个名为s_DataReceiver的全局静态变量,类型为DataReceiver,用于保存DataReceiver的实例。在getInstance()函数,返回s_DataReceiver的值。在构造函数,将父对象传递给QThread的构造函数。stop()函数,调用requestInterruption()函数请求线程断。在run()函数,通过互斥锁mutex保证线程安全,判断数据池是否为空,如果不为空,则取出第一个数据并发出信号oneDataReady()。最后,通过调用msleep()函数短暂睡眠,让出线程。 使用该类的方法是,首先获取DataReceiver的实例,然后调用start()函数启动线程。在接收到数据后,将数据添加到数据池。可以通过连接信号oneDataReady()来处理接收到的数据。 以上是在Qt实现多线程数据收发的简要介绍。\[1\]\[3\] #### 引用[.reference_title] - *1* *3* [一种基于Qt多线程的数据接收方案](https://blog.csdn.net/iriczhao/article/details/121503545)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Qt工作笔记-UDP多线程数据处理及发送(简单实例)](https://blog.csdn.net/weixin_39786534/article/details/111545637)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值