<Qt> 系统 - 多线程

目录

一、Qt多线程

(一)常用API

(二)使用Qt多线程

(三)线程安全

1. 互斥锁

 2. 条件变量

3. 信号量


一、Qt多线程

Qt 多线程和 Linux 中的多线程本质是一样的。在 Linux 中使用的API是Linux系统提供的pthread库,Qt 也重新封装了。

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

(一)常用API

API说明
run()线程的⼊⼝函数…
start()通过调⽤ run() 开始执⾏线程。操作系统将根据优先级参数调度线程。如果线程已经在运⾏,这个函数什么也不做。
currentThread()返回⼀个指向管理当前执⾏线程的 QThread的指针。
isRunning()如果线程正在运⾏则返回true;否则返回false。
sleep() / msleep() /usleep()使线程休眠,单位为秒 / 毫秒 / 微秒
wait()阻塞线程,直到满⾜以下任何⼀个条件:与此 QThread 对象关联的线程已经完成执⾏(即当它从run()返回时)。如果线程已经完成,这个函数将返回 true。如果线程尚未启动,它也返回 true。已经过了⼏毫秒。如果时间是 ULONG_MAX(默认值),那么等待永远不会超时(线程必须从run()返回)。如果等待超时,此函数将返回 false。这提供了与 POSIX pthread_join() 函数类似的功能。
terminate()终⽌线程的执⾏。线程可以⽴即终⽌,也可以不⽴即终⽌,这取决于操作系统的调度策略。在terminate() 之后使⽤ QThread::wait() 来确保。
finished()当线程结束时会发出该信号,可以通过该信号来实现线程的清理⼯作。这个信号可以连接到QObject::deleteLater(),以释放该线程中的对象。

(二)使用Qt多线程

Qt多线程一般使用步骤:

  1. 创建一个类,并继承自QThread
  2. run函数为线程的入口函数,因此要根据自己的需要重写run函数;
  3. 通过这个新创建的类实例化一个新对象;
  4. 通过这个新对象来调用其start函数,以启动线程;
  5. 这个启动的线程会以重写的run函数为入口函数开始运行;
  6. 最后回收线程。

代码示例:通过线程来实现定时器

在ui界面放置一个Lcd控件:

新建一个线程类:

 

// 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

#include "thread.h"

Thread::Thread()
{

}

void Thread::run()
{
    // 当到1s时,通过信号槽来通知主线程,负责更新界面内容
    for(int i = 0; i < 10; i++)
    {
        // sleep本身是QThread的成员函数,可直接调用
        sleep(1);
        // 发送一个信号来通知主线程
        emit notify();
    }
}

 

// widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include "thread.h"

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

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

private:
    Ui::Widget *ui;

    Thread thread;
    void handle();
};
#endif // WIDGET_H


// widget.cpp

#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();
}

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

void Widget::handle()
{
    // 此处修改界面内容
    int value = ui->lcdNumber->intValue();
    value--;
    ui->lcdNumber->display(value);
}

 运行效果如下:

我们之前提到多线程,主要还是站在服务器开发的角度考虑的,就是利用了CPU多核的资源,或者双路CPU,从而达到高效的处理。

       那么客户端多线程的意义不在这里,对于客户端用户来说,体验感是主要的,还是要通过多线程的方式执行一些耗时的等待IO的操作,避免主线程卡死,比如客户端和服务端进行上传和下载一个很大的文件,传输需要消耗很长时间,这种密集IO操作就会使程序被系统阻塞,所以只要被阻塞,用户的操作也就无法响应。

       所以更好的做法是使用单独的线程来处理密集IO操作,主线程主要负责事件循环,负责处理用户的操作。

说明:

  1. 线程函数内部不允许操作 UI 图形界面,⼀般用数据处理。
  2. connect() 函数第五个参数表示的为连接的方式,且只有在多线程的时候才意义。

connect() 函数第五个参数为 Qt::ConnectionType,用于指定信号和槽的连接类型。同时影响信号的传递方式和槽函数的执行顺序。

Qt::ConnectionType 提供了以下五种方式:

(三)线程安全

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

  • 互斥锁:QMutex、QMutexLocker
  • 条件变量:QWaitCondition
  • 信号量:QSemaphore
  • 读写锁:QReadLocker、QWriteLocker、QReadWriteLock

1. 互斥锁

QMutex 是 Qt 框架提供的互斥锁类,用于保护共享资源的访问,实现线程间的互斥操作。在多线程环境下,通过互斥锁来控制对共享数据的访问,确保线程安全。

QMutex mutex;
 
mutex.lock(); //上锁
 
//访问共享资源
//...
 
mutex.unlock(); //解锁

QMutexLocker 是 QMutex 的辅助类,使用 RAII(Resource Acquisition Is Initialization)方式对互斥锁进行上锁和解锁操作。简化对互斥锁的上锁和解锁操作,避免忘记解锁导致的死锁等问题。

QMutex mutex;
{
    QMutexLocker locker(&mutex); //在作⽤域内⾃动上锁
 
    //访问共享资源
    //...
 
} //在作⽤域结束时⾃动解锁

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

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

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

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

QReadWriteLock rwLock;
//在读操作中使⽤读锁
{
    QReadLocker locker(&rwLock); //在作⽤域内⾃动上读锁
 
    //读取共享资源
    //...
 
} //在作⽤域结束时⾃动解读锁
 
 
 
//在写操作中使⽤写锁
{
    QWriteLocker locker(&rwLock); //在作⽤域内⾃动上写锁
 
    //修改共享资源
    //...
 
} //在作⽤域结束时⾃动解写锁

代码示例:使用互斥锁让两个线程累加计数

// thread.h

#ifndef THREAD_H
#define THREAD_H

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

class Thread : public QThread
{
    Q_OBJECT
public:
    Thread();

    // 添加一个static成员
    static int num;
    // 创建锁对象
    static QMutex mutex;

    // 重写父类的run方法
    void run();

signals:
    void notify();
};

#endif // THREAD_H

// thread.cpp

#include "thread.h"

int Thread::num = 0;
QMutex Thread::mutex;

Thread::Thread()
{

}

void Thread::run()
{
    for(int i = 0; i < 50000; i++)
    {
        mutex.lock();
        num++;
        mutex.unlock();
    }
}
#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();

    // 加上线程的等待,让主线程等待这两个线程执行结束
    t1.wait();
    t2.wait();

    // 打印结果
    qDebug() << Thread::num;
}

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

 运行效果如下:

代码示例2:在示例 1 的基础上使用 QMutexLocker

话又说回来,作为一个C++程序员,内存泄漏是一个很大的问题,所以一定要注意unlock的问题,如果代码执行过程中return了,或者因为抛异常,都可能导致无法调用unlock的问题,同理,释放内存也是这样的,所以为了解决这样的问题,C++中就出现了智能指针,同样是使用RAII的机制。Qt 中的QMutexLocker就是QMutex的智能指针

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


Qt 的锁和 C++ 标准库中的锁,本质上都是封装的系统提供的锁。编写多线程程序时,可以使用 Qt 的锁,也可以使用 C++ 的锁。

C++ 的锁能否锁 Qt 的线程呢?

可以,但不建议混着使用。

 2. 条件变量

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

在多线程编程中,假设除了等待操作系统正在执行的线程之外,某个线程还必须等待某些条件满足才能执行,这时就会出现问题。这种情况下,线程会很自然地使用锁的机制来阻塞其他线程,因为这只是线程的轮流使用,并且该线程等待某些特定条件,人们会认为需要等待条件的线程,在释放互斥锁或读写锁之后进入了睡眠状态,这样其他线程就可以继续运行。当条件满足时,等待条件的线程将被另一个线程唤醒。在 Qt 中,专门提供了 QWaitCondition 类来解决像上述这样的问题。

QWaitCondition 是 Qt 框架提供的条件变量类,用于线程之间的消息通信和同步。某个条件满足时等待或唤醒线程,用于线程的同步和协调。

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

3. 信号量

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

信号量类似于增强的互斥锁,不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值