zQt多线程

Qt多线程和Linux中的线程本质是一个东西。在Qt中,多线程的处理⼀般是通过QThread类来实现。QThread代表一个在应用程序中可以独立控制的线程,也可以和进程中的其他线程共享数据。QThread对象管理程序中的一个控制线程。

要想创建线程,就需要先创建出这样类的实例(对象),创建线程的时候,需要重点指定线程的入口函数,在Qt中指定入口函数是通过创建一个QThread的子类,重写其中的run函数来完成(多态)。C++中这种做法并不常见,相比之下std::thread(C++11引入的)直接指定回调的方式更常见一些。

QThread常用API

run()

线程的入口函数

start()

 通过调用run()开始执行线程。操作系统将根据优先级参数调度线程。如果线程已经在运行,这个函数什么也不做

currentThread()

返回⼀个指向管理当前执行线程的QThread的指针。

isRunning()

如果线程正在运行则返回true;否则返回false。

sleep()/msleep()/usleep()

使线程休眠,单位为秒/毫秒/微秒

wait()        这提供了与POSIX  pthread_join()函数类似的功能。

阻塞线程,直到满足以下任何⼀个条件:

与此QThread对象关联的线程已经完成执行(即当它从run()返回时)。如果线程已经完成,这个函数将返回true。如果线程尚未启动,它也返回true。

已经过了几毫秒。如果时间是ULONG_MAX(默认值),那么等待永远不会超时(线程必须从run()返回)。如果等待超时,此函数将返回false。

terminate()

终止线程的执行。线程可以立即终止,也可以不立即终止,这取决于操作系统的调度策略。在terminate()之后用QThread::wait()来确保。

finished() 当线程结束时会发出该信号,可以通过该信号来实现线程的清理工作。

代码示例

基于多线程来完成一个倒计时的程序:一个线程每循环1次sleep1s,sleep完成然后可以更新界面

先创建一个子类继承QThread

然后重写run函数

thread.h

#ifndef THREAD_H
#define THREAD_H

#include <QWidget>
#include<QThread>
class Thread : public QThread
{
    Q_OBJECT
public:
    Thread();
    //重写父类的run 方法
    void run();
signals:
    void notify();//声明一个信号
};

#endif // THREAD_H

thread.cpp

run函数中等待1秒后发送我们自定义的信号通知主线程

#include "thread.h"

Thread::Thread()
{

}

void Thread::run()
{
    //不可以在这里直接修改界面,Qt中只有主线程能修改啊
    for(int i=0;i<10;i++)
    {
        sleep(1);
        //发送信号通知主线程修改
        emit notify();
    }
}

主线程通过connect连接信号槽,调用slot函数来修改界面

#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    //连接信号槽,通过槽函数更新界面
    connect(&thread,&Thread::notify,this,&Widget::handle);
    
    thread.start();
}

void Widget::handle()
{
    //在这里修改界面即可
    int value = ui->lcdNumber->intValue();
    value--;
    ui->lcdNumber->display(value);
}

QThread的应用场景

之前学习多线程,主要还是站在服务器开发的角度来看待的——为了充分利用多核CPU的计算资源

那么在客户端中,多线程仍然非常有意义,只不过侧重点不同了,对于普通用户来说,"使用体验"是一个非常重要的话题。如果"非常快"的代价是"系统很卡"那么用户大概率是不会买账的(虽然家用电脑的CPU也是多核的,客户端很少会用多线程把cpu的计算资源吃完)。所以客户端中的多线程主要是用于执行一些耗时等待IO的操作,避免主线程被卡死、避免对用户造成一些不好的体验

举个例子:客户端经常会和服务器进行网络通,比如客户端要上传/下载一个很大的文件(密集的IO操作),传输需要消耗好久。这种密集的IO就会使程序被系统阻塞,挂起。那么一旦进程都被挂起来,此时意味着用户进行的各种操作,程序都无法响应。(启动一些大型的游戏的时候,这个启动过程需要从文件/网络加载大量的资源,如果在这个时候你鼠标狂点窗口,很可能这个窗口就僵住了,然后系统提示”此窗口不能响应,是否要强制结束...")

因此,相比之下更好的做法是:使用单独的线程,来处理这种密集的IO操作,那么要挂起也是挂这个新的线程,主线程负责事件循环,负责处理用户的各种操作。

线程安全问题

Qt中实现线程互斥和同步常用的类有:

互斥锁:QMutex、QMutexLocker

条件变量:QWaitCondition

信号量:QSemaphore

读写锁:QReadLocker、QWriteLocker、QReadWriteLock

互斥锁

互斥锁是⼀种保护和防止多个线程同时访问(变成串行访问)同一对象实例的方法,在Qt中,互斥锁主要是通过 QMutex类来处理。QMutex中lock用来加锁,unlock用来解锁

代码示例:

线程还是先创建一个子类继承自QThread,然后重写run,至于两个线程访问同一个对象,可以在子类中创建一个static 成员。

.h

#ifndef THREAD_H
#define THREAD_H

#include <QWidget>
#include<QThread>
class Thread : public QThread
{
    Q_OBJECT
public:
    Thread();
    //添加一个static成员,可以让两线程修改
    static int total;
    //重写run
    void run();
};

#endif // THREAD_H

每个线程都循环的加total

#include "thread.h"
int Thread:: total = 0;
Thread::Thread()
{

}

void Thread::run()
{
    for(int i=0;i<50000;i++)
    {
        total++;
    }
}

在widget的构造中创建俩线程对象,打印看看。注意:widget还是个主线程呢,要让它等一下两个线程,别还没加完就打印出来了

#include "widget.h"
#include "ui_widget.h"
#include"thread.h"
#include<QDebug>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    //创建两线程对象
    Thread t1;
    Thread t2;
    t1.start();
    t2.start();
    
    //widget还是个主线程呢,要让它等一下两个线程,别还没加完就打印出来了
    t1.wait();
    t2.wait();
    
    //打印一下
    qDebug()<<Thread::total;
}

以上是没加锁的情况,明显存在线程安全问题。

#include <QWidget>
#include<QThread>
#include<QMutex>

class Thread : public QThread
{
    Q_OBJECT
public:
    Thread();
    //添加一个static成员,可以让两线程修改
    static int total;
    //重写run
    void run();
    //锁
    static QMutex mutex;
};
#include "thread.h"
int Thread:: total = 0;
QMutex Thread::mutex;
Thread::Thread()
{

}

void Thread::run()
{
    for(int i=0;i<50000;i++)
    {
        mutex.lock();
        total++;
        mutex.unlock();
    }
}

C++11中为了解决释放锁的困难问题(if判断后return 、抛异常这样的场景容易忘记释放锁,释放动态内存也存在类似的问题,C++通过智能指针解决),就引入了std::lock_guard(相当于std::mutex的智能指针,借助RAII机制)。

void function()
{
    std::lock_guard guard(mutex);

    //执行各种逻辑...

    
}

大括号执行完毕guard变量 的生命周期结束就会在析构的时候,执行unlock了

这样的方案Qt也参考过来了,叫做QMutexLocker

#include "thread.h"
#include<QMutexLocker>
int Thread:: total = 0;
QMutex Thread::mutex;
Thread::Thread()
{

}

void Thread::run()
{
    for(int i=0;i<50000;i++)
    {
        QMutexLocker locker(&mutex);
        //mutex.lock();
        total++;
        //mutex.unlock();
    }
}

QReadWriteLocker、QReadLocker、QWriteLocker特点:

QReadWriteLock是读写锁类,用于控制读和写的并发访问。

QReadLocker 用于读操作上锁,允许多个线程同时读取共享资源。

QWriteLocker 用于写操作上锁,只允许一个线程写入共享资源。

用途:在某些情况下,多个线程可以同时读取共享数据,但只有一个线程能够进行写操作。读写锁提供了更高效的并发访问方式

条件变量和信号量

这里的条件变量、信号量,和Linux中谈到的条件变量/信号量完全一致。

多线程中,假设除了等待操作系统正在执行的线程之外,某个线程还必须等待某些条件满足才能执行,防止他占着茅坑不拉屎(自己不用,还调用锁来阻塞其他线程)。让需要等待条件的线程在释放互斥锁或读写锁之后进入了睡眠状态,然后其他线程就能用了,当条件满足时,才唤醒那个线程。

多个线程之间的调度是无序的,为了一定程度上能够干预线程之间的执行顺序引入了条件变量

QWaitCondition,提供了wait,wake,wakeAll等函数

QMutex mutex;
QWaitCondition condition;
//在等待线程中 
mutex.lock();
//检查条件是否满⾜,若不满⾜则等待 
while (!conditionFullfilled()) 
{
 condition.wait(&mutex); //等待条件满⾜并释放锁 
}
//条件满⾜后继续执⾏ 
//...
mutex.unlock();
//在改变条件的线程中 
mutex.lock();
//改变条件 
changeCondition();
condition.wakeAll(); //唤醒等待的线程 
mutex.unlock();

信号量

有时在多线程编程中,需要确保多个线程可以相应的访问一个数量有限的相同资源。例如,运行程序的设备可能是非常有限的内存,因此我们更希望需要大量内存的线程将这一事实考虑在内,并根据可用的内存数量进行相关操作,多线程编程中类似问题通常用信号量来处理。信号量类似于增强的互斥锁,不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量。

特点:QSemaphore是Qt框架提供的计数信号量类,用于控制同时访问共享资源的线程数量。 用途:限制并发线程数量,用于解决一些资源有限的问题。

QSemaphore semaphore(2); //同时允许两个线程访问共享资源 
//在需要访问共享资源的线程中 
semaphore.acquire(); //尝试获取信号量,若已满则阻塞 
//访问共享资源 
//...
semaphore.release(); //释放信号量 
//在另⼀个线程中进⾏类似操作 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐