06-多线程(qt)

Tips:

一个进程可能包含多个线程;

进程结束,其包含的线程全部结束;

线程结束,进程不一定结束。

引入多线程的原因:

应用程序在某些情况下需要处理比较复杂的逻辑,如果只有一个线程去处理,会导致窗口卡顿,无法处理用户的相关操作。

这种情况下就需要使用多线程,其中一个线程处理窗口事件,其他线程进行逻辑运算,提高用户体验,提升程序的执行效率。

Qt中使用多线程的注意点

  • Qt的默认线程为窗口线程(主线程):负责窗口事件处理或窗口控件数据的更新;
  • 子线程负责后台的业务逻辑,子线程不能对窗口对象做任何的操作,这些事交给窗口线程;
  • 主线程和子线程之间进行数据传递,需要使用信号槽机制。

Qt提供的线程类,QThread ,通过这个类可以创建子线程

Qt提供了两种创建子线程的方式。

// 常用API
// 构造函数
QThread::QThread(QObject *parent = Q_NULLPTR);
// 判断线程中的任务是否处理完毕
bool QThread::isFinished() const;
// 判断子线程是否在执行任务
bool QThread::isRunning() const;
// 开始时发送的start()信号,start()默认调用run()方法
.started() // 开始的信号
.finished() // 结束的信号
.terminated() // 终止的信号

// 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()函数在线程内部被调用。

多线程的使用

  • 方式一

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

    class MyThread:public QThread{
        ......
    }
    

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

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

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

    MyThread *subThread = new MyThread;
    

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

    subThread->start();
    

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

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

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

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

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

    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; // 正确写法
    

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

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

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

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

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

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

线程池

实现多线程的方式

  1. 继承QThread,重写run()函数
  2. 使用moveToThread将一个继承QObject的子类移至线程,内部槽函数均在线程中执行
  3. 使用QThreadPool,搭配QRunnable(线程池)
  4. 使用QtConcurrent(线程池)

使用类QThread

代码完全在一个独立的线程中运行,需要继承QThread类,并且重写QThread类的run()方法。实现方式比较简单,线程在执行完run()函数之后退出。

class WorkerThread : pubilic QThread{
    Q_OBJECT
protected:
    void run() Q_DECL_OVERRIDE{
        QString result;
        
        emit resultReady(result);
    }
private:
signals:
    void resultReady(const QString &s);
};

void MyObject::StartWorkInAThread(){
    workerThread *workerThread = new WorkerThread(this);
    connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
    connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
    workerThread->start();
}

优点

可以使用信号槽进行通信

缺点

  • 实例化的子类是在创建线程的旧线程中,不是在新创建的子线程中,因此,与该线程相关所有槽队列(在槽存在的情况下),都会在创建的旧线程中执行,不能直接在新建的线程中使用槽,如果需要,则需要借助worker-object实现。
  • 每创建一个线程都需要继承QThread,实现一个新的子类,使用不便捷。
  • 需要自己管理资源,线程的创建和释放,都需要自己手动管理,并且,频繁的创建和删除会造成比较大的内存开销。
  • 实例化子类的构造函数和run()函数在不同的线程中运行,因此,假设有成员变量两个函数中都能访问,则需要注意,多线程中的资源的访问问题。

适用场景

线程不会被频繁的创建和删除,常驻内存的线程。

使用QThread类中的moveToThread方法

创建一个继承QObject的类(MyObject),并把创建的MyObject类通过方法movetothread到创建好的子线程中,然后start子线程,这样就实现了一个子线程。主线程通过发送信号,调用MyObject中的方法,从而实现子线程中的计算,不需要继承QThread,不需要重写run()函数。

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 &);
};

Tips:

  • 只有在槽中执行的操作才是在线程中执行的,所以需要通过连接信号槽的方式来实现
  • 如果object对象存在父对象,不能将其移到子线程中执行
  • 相对来说比较方便,适用于一些比较复杂的业务中

QThreadPoolQRunnable

QRunnable 是所有可执行对象的基类。 QRunnable类是一个接口, 用于表示需要执行的任务或代码段, 具体任务在run() 函数内部实现。实现的过程跟QThread类似,也是需要通过继承来实现。

可以通过搭配QThreadPool,使得其在单独的线程中执行代码。如果autoDelete()返回true(默认值),QThreadPool将自动删除QRunnable。使用setAutoDelete()来更改自动删除标记。

QThreadPool支持通过在run()函数中调用QThreadPool::tryStart(this)来多次执行同一个QRunnable。如果autoDelete被启用,QRunnable将在最后一个线程退出run函数时被删除。

如果启用了autoDelete,使用相同的QRunnable多次调用QThreadPool::start()会造成多线程访问同一资源,形成竞争,因此不推荐使用。

class Runnable : public QRunnable
{
    //Q_OBJECT		QRunnable 不是QObject的子类,因此在这儿需要注意
public:

    ~Runnable()
    {
        qDebug() << "~Runnable..."  << endl;
    }
    void run()
    {
        qDebug() << " Runnable run thread id :" << QThread::currentThreadId() << endl;
        //...
    }
};

void MyObject::startWorkInRunable()
{
    qDebug() << "main thread id :" << QThread::currentThreadId() << endl;
    Runnable run;
    QThreadPool::globalInstance()->start(&run);
}

优点:

  • 不用资源管理,QThreadPool 启动线程执行完成后会自动释放

缺点:

  • 可能会形成多线程资源竞争
  • 不能使用信号槽(信号槽只能在QObject中使用)

适用场景:

  • QRunnable适用于线程任务量比较大,需要频繁创建线程。QRunnable能有效减少内存开销。

使用QtConcurrent

Concurrent是并发的意思,QtConcurrent是一个命名空间,提供了一些高级的 API,使得在编写多线程的时候,无需使用低级线程原语,如读写锁,等待条件或信号。使用QtConcurrent编写的程序会根据可用的处理器内核数自动调整使用的线程数。这意味着今后编写的应用程序将在未来部署在多核系统上时继续扩展。

QtConcurrent::run能够方便快捷的将任务丢到子线程中去执行,无需继承任何类,也不需要重写函数,使用非常简单。

QFuture<T> run(Function function, ...)
QFuture<T> run(QThreadPool *pool, Function function, ...)   //Qt5.4引入
//T与函数的返回值类型相同。非void类型返回值可以通过QFuture::result()函数访问。

#include <QtConcurrent/QtConCurrent>
#include <QFuture>

QFuture<void> func = QtConcurrent::run(this, &MyClass::measure, param);

void MyClass::measure(const QString& param)
{
	//do something...
}

调用非常简单,这样我们设置的函数会在一个单独的线程中执行,这个线程是从QThreadPool池中获取的,因此,该线程可能不会直接执行(如果QThreadPool池中没有空闲的线程),当QThreadPool池中有空闲的线程后会执行,执行完成后会将线程还给QThreadPool。

QtConcurrent::run()返回的QFuture不支持取消、暂停或进度报告。返回的QFuture只能用于查询函数的运行/完成状态和返回值。非void类型返回值可使用QFuture::result()访问获取。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值