Qt开发——系统相关2(Qt多线程)
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(); //释放信号量
//在另⼀个线程中进⾏类似操作
更多推荐


所有评论(0)