QT的多线程管理与互斥锁QMUTEX保证线程安全

文章目录


前言

一、QT多线程的实现方式

1.重载run()函数
(1)实现过程
QThread是Qt线程中有一个公共的抽象类,所有的线程类都是从QThread抽象类中派生的,需要实现QThread中的虚函数run(),通过start()函数来调用run函数。
简单示例:新建的线程类,修改继承关系为QThread后,重载run()函数,在run()函数执行需要的操作。


void mythread::run()
{
    for(int i=0;i<10;i++) {
        qDebug() << "aa";
        sleep(1);
    }
    emit isDone();  //发送完成信号
}
调用时,声明自定义的线程类,通过start()函数启动线程。

(2)执行过程:
int exec() [protected]
进入事件循环并等待直到调用exit(),返回值是通过调用exit()来获得,如果调用成功则返回0。
void run() [virtual protected]
线程的起点,在调用start()之后,新创建的线程就会调用run函数,默认实现调用exec(),大多数需要重新实现run函数,便于管理自己的线程。run函数返回时,线程的执行将结束
void quit();
通知线程事件循环退出,返回0表示成功,相当于调用了QThread::exit(0)。
2.moveToThread方式
创建一个集成QObject的类(myObject),new 一个QThread对象,并调用moveToThread()函数,将创建的myObject类移动到子线程中,子线程(myObject)通过发发送信号,利用信号槽机制,与主线程进行通信。

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

二、线程中互斥锁的使用

1.QMutex简介

QMutex类提供的是线程之间的访问顺序化。QMutex的目的是保护一个对象、数据结构或者代码段,所以同一时间只有一个线程可以访问它。如果使用Mutex锁那么多个线程在访问一段代码的时候是存在阻塞的,一个执行完毕下一个线程才会继续执行

2.QMutex使用

QMutex::QMutex ( bool recursive = FALSE )
构造一个新的互斥量。这个互斥量是在没有锁定的状态下创建的。如果recursive为真,就构造一个递归互斥量,如果recursive为假(默认值),就构造一个普通互斥量。对于一个递归互斥量,一个线程可以锁定一个互斥量多次并且只有在相同数量的unlock()调用之后,它才会被解锁
void QMutex::lock ()
试图锁定互斥量。如果另一个线程已经锁定这个互斥量,那么这次调用将阻塞直到那个线程把它解锁。
unlock()进行解锁。
bool QMutex::tryLock ()
试图锁定互斥量。如果锁被得到,这个函数返回真。如果另一个进程已经锁定了这个互斥量,这个函数返回假,而不是一直等到这个锁可用为止


三.QSemaphore使用

1.QSemaphore简介

QSemaphore也可以被用来使线程的执行顺序化,和QMutex的方法相似。信号量和互斥量的不同在于,QSemaphore信号量可以在同一时间被多于一个的线程访问。通常将QSemaphore具体使用定义为 生产者/消费者 模式
acquire()函数用于获取n个资源,当没有足够的资源时调用者将被阻塞直到有足够的可用资源。
release(n)函数用于释放n个资源。

2.QSemaphore使用

​ 信号量的一个典型应用场景是:控制对由生产者线程和消费者线程共享的循环缓冲区的访问。
下文将描述这个例子:

​ 生产者写入数据到循环缓冲区,直到到达缓冲区的末端后生产者将从头部重新开始进行数据写入,覆盖现有的数据。消费者线程在数据生成时读取数据,并将其打印到标准输出流中(即显示出来)。

​ 信号量比互斥锁有更高级别的并发性。如果对循环缓冲区的访问是由QMutex来保护的,那么消费者线程不能与生产者线程同时访问缓冲区。然而,实际可以让两个线程同时在缓冲区的不同部分上进行操作,不会对缓冲区造成影响。
摘自于:
信号量

(1)全局变量定义:

//生产者将生成的数据量
const int DataSize = 100000;

//循环缓冲区的大小,该值小于DataSize
const int BufferSize = 8192;
//循环缓冲区
char buffer[BufferSize];

QSemaphore freeBytes(BufferSize);
QSemaphore usedBytes;

由于BufferSize小于DataSize,这意味着在某一时刻生产者将到达缓冲区的末端并从缓冲区的头部重新开始执行数据写入操作。

​ 为了同步生产者和消费者,定义了两个信号量:

​ 1)freeBytes信号量控制缓冲区的“空闲”区域(生产者还没有填满数据或消费者已经读过的区域)。

​ 2)usedBytes信号量控制缓冲区的“已用”区域(生产者已经填满但消费者还没有读取的区域)。
在这里插入图片描述
​ 这些信号量一起确保了生产者永远不会比消费者超前超过BufferSize字节,并且消费者永远不会读取生产者还没有生成的数据
freeBytes信号量用BufferSize初始化,因为最初整个缓冲区是空的。usedBytes信号量被初始化为0(如果没有指定,则为默认值)
(2)生产者类

class Producer : public QThread
{
public:
    void run() override
    {
        for (int i = 0; i < DataSize; ++i) {
            freeBytes.acquire();
            buffer[i % BufferSize] = "ACGT"[QRandomGenerator::global()->bounded(4)];
            usedBytes.release();
        }
    }
};

生产者生成DataSize字节的数据。在将一个字节写入循环缓冲区之前,它必须使用freeBytes信号量获取一个“free”字节。如果消费者没有跟上生产者的步伐,QSemaphore::acquire()调用可能会阻塞。

​ 最后,生产者使用usedBytes信号量释放一个字节。随后,“free”字节已成功转换为“used”字节,准备供消费者读取啦。
(3)消费者类

class Consumer : public QThread
{
    Q_OBJECT
public:
    void run() override
    {
        for (int i = 0; i < DataSize; ++i) {
            usedBytes.acquire();
            fprintf(stderr, "%c", buffer[i % BufferSize]);
            freeBytes.release();
        }
        fprintf(stderr, "\n");
    }
};

​ 消费者代码类似于生产者线程,实现步骤是:获得一个“used”的字节并释放一个“free”的字节
(4)主函数

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    Producer producer;
    Consumer consumer;
    producer.start();
    consumer.start();
    producer.wait();
    consumer.wait();
    return 0;
}

​ 在main()中,创建两个线程,并调用QThread::wait()来确保两个线程在退出之前都有足够时间保证运行完成

3.QSemaphore总结

生产者线程是唯一一个可以做任何事情的线程;消费者线程将被阻塞,等待usedBytes信号量被释放(它的初始available()计数是0)。一旦生产者把一个字节放入缓冲区,freeBytes.available()是BufferSize - 1,而usedBytes.available()是1。此时可能会发生两件事:要么由消费者线程接管并读取该字节,要么由生产者线程生成第二个字节。

​ 本例中提供的【生产者-消费者】模型是:编写高并发多线程应用程序的基本模型。在拥有多处理器的机器上,由于两个线程可以同时在缓冲区的不同部分上进行操作,因此程序的运行速度可能是基于互斥锁程序的两倍,这大大提高了程序的运行速度。

​ 注意,获取和释放QSemaphore是有代价的。在实践中,可能需将缓冲区划分为块,并对块进行操作。缓冲区大小参数也是一个必须根据实验仔细选择的参数。

四.线程与信号与槽

取决于信号与槽的第五个参数:
1、Qt::AutoConnection: 默认值,使用这个值则连接类型会在信号发送时决定。如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。
2、Qt::DirectConnection:槽函数会在信号发送的时候直接被调用,槽函数和信号发送者在同一线程。效果看上去就像是直接在信号发送位置调用了槽函数,效果上看起来像函数调用,同步执行。
emit语句后面的代码将在与信号关联的所有槽函数执行完毕后才被执行。
无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行。
3、Qt::QueuedConnection:信号发出后,信号会暂时被放到一个消息队列中,需等到接收对象所属线程的事件循环取得控制权时才取得该信号,然后执行和信号关联的槽函数,这种方式既可以在同一线程内传递消息也可以跨线程操作。
emit语句后的代码将在发出信号后立即被执行,无需等待槽函数执行完毕
槽函数在接收者所依附线程执行。
4、Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。而且接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
5、Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是为了避免重复连接。
一般qt connect的第五个参数会在多线程中运用到,需要注意的是:
QThread是用来管理线程的,QThread对象所依附的线程和所管理的线程并不是同一个概念。QThread所依附的线程,就是创建QThread对象的线程;QThread 所管理的线程,就是run启动的线程,也就是新建线程。

五.QWaitCondition

QWaitConditon也是用来同步线程的。从名字来看是等待条件,意思就是线程阻塞在等待条件的地方,直到条件满足才继续执行下去。等待条件的线程可以是一个或者多个。用QWaitCondition的函数表示过程如下:

1.等待条件的线程调用QWaitCondition::wait()阻塞。

2.实现条件的线程通过计算完成条件后调用QWaitConditon::wakeOne()或者QWaitCondition::wakeAll()。

3.当2中调用wake之后,继续执行wait之后的操作。

其中wakeOne会随机唤醒等待的线程中的一个。wakeAll会唤醒所有的等待线程。需要用到一个互斥量作为参数,而这个互斥量的状态必须是locked的。

当调用这一句waitcondition.wait(&mutex) 在等待触发条件的时候,此时的mutex已经被设置为unlocked状态。当条件满足wait语句朝下执行的时候,mutex又被设置为locked状态。

这个时候第一个线程会释放锁资源,自己处于条件等待状态。那么在上面等待锁资源的第二个线程将会获得所资源,然后有执行到wait处,以此类推 第三、第四、第五线程一次获得锁资源,最后所有线程都会等待在wait处。
当条件变量满足条件被其他线程唤醒的时候即执行wakeall或者wakeone.执行wakeall的时候所有等待线程都会被唤醒,继续往下执行,那么大家担心以后的语句即2处会不会有同步问题呢?不会,因为每个线程唤醒后第一个线程(或者说其中有一个线程)
获取mutex锁资源,然后再执行下面的语句,其他被唤醒的线程只能在这个mutex等待锁资源。
整个过程可以理解为
这一段代码多个线程会在多出集结等待资源,但这个代码的执行始终是互斥的。

六.QT的事件循环

引自事件循环
QT的程序运行流程,叫做“事件驱动”式的程序。一般的Qt程序,main函数中都会有一个QCoreApplication/QGuiApplication/QApplication,并在末尾调用exec。Application中的这个EventLoop,叫做“事件循环”,所有的事件分发、事件处理都从这里开始。Application还提供了sendEvent和poseEvent两个函数,分别用来发送事件。sendEvent发出的事件会立即被处理,即“同步”执行。poseEvent发送的事件会被加入事件队列,在下一轮事件循环时才处理,即“异步”执行。

1.Qt是事件驱动的,怎么理解这句话
Qt将系统产生的信号(软件中断)转换成Qt事件,并且将事件封装成类,所有的事件类都是由QEvent派生的,事件的产生和处理就是Qt程序的主轴,且伴随整个程序的运行周期。因此说Qt是事件驱动的。
2.Qt事件由谁产生的?
事件有两个来源:程序内部和程序外部,多数情况下来自操作系统并且通过spontaneous()函数返回true来获知事件来自程序外部,当spontaneous()函数返回false时说明事件来自程序内部。
在这里插入图片描述

在这里插入图片描述

七.线程之可重入与线程安全

可重入:可以被多个线程同时调用,那么这个类被称为是“可重入”的。
线程安全:假如不同的线程作用在同一个实例上仍可以正常工作。
在查看Qt的帮助文档时,在很多类的开始都写着“All functions in this class are reentrant”,或者“All functions in this class are thread-safe”。在Qt文档中,术语“可重入(reentrant)”和“线程安全(thread-safe)”用来标记类和函数,来表明怎样在多线程应用程序中使用它们:
一个线程安全的函数可以同时被多个线程调用,即便是这些调用使用了共享数据。因为该共享数据的所有实例都被序列化了。
一个可重入的函数也可以同时被多个线程调用,但是只能是在每个调用使用自己的数据时。

八.线程池QthreadPool

线程池维护一定数量的线程,使用时,将指定函数传递给线程池,线程池会在线程中执行任务。用来管理 QThreads,经过测试QThreadPool线程池函数并不是安全线程,多个线程操作还是会出现抢资源现象,同步还是需要互斥锁或者信号量来同步。
线程池函数:
int activeThreadCount() const //当前的活动线程数量
void clear()//清除所有当前排队但未开始运行的任务
int expiryTimeout() const//线程长时间未使用将会自动退出节约资源,此函数返回等待时间
int maxThreadCount() const//线程池可维护的最大线程数量
void releaseThread()//释放被保留的线程
void reserveThread()//保留线程,此线程将不会占用最大线程数量,从而可能会引起当前活动线程数量大于最大线程数量的情况
void setExpiryTimeout(int expiryTimeout)//设置线程回收的等待时间
void setMaxThreadCount(int maxThreadCount)//设置最大线程数量
void setStackSize(uint stackSize)//此属性包含线程池工作线程的堆栈大小。
uint stackSize() const//堆大小
void start(QRunnable *runnable, int priority = 0)//加入一个运算到队列,注意start不一定立刻启动,只是插入到队列,排到了才会开始运行。需要传入QRunnable
bool tryStart(QRunnable *runnable)//尝试启动一个
bool tryTake(QRunnable *runnable)//删除队列中的一个QRunnable,若当前QRunnable 未启动则返回成功,正在运行则返回失败
bool waitForDone(int?msecs?=?-1)//等待所有线程运行结束并退出,参数为等待时间-1表示一直等待到最后一个线程退出

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值