QThread 多线程
Qt中,多线程处理一般通过 QThread类 实现
QThread 用于在多线程程序中处理并发任务,也可以和进程宏的其他线程共享数据
常用API
QThread 的API和 C++ 并不同,不是使用回调的方式,而是使用多态,仿照 Java 接口设计
API | 说明 |
---|---|
run() | 线程的入口函数 |
start() | 通过调用 run() 开始执行线程。操作系统将根据优先级参数调度线程,如果线程已经在运行,这个函数什么也不做 |
currentThread() | 返回一个指向管理当前执行线程的 QThread 的指针 |
isRunning() | 如果线程正在运行则返回 true;否则返回 false |
sleep() / msleep() / usleep() | 使线程修改,单位为 秒 / 毫秒 / 微秒 |
wait() | 阻塞线程,直到 与此 QThread 对象关联的线程已经完成执行(即从 run() 返回)。如果线程已经完成,这个函数返回 true。如果线程尚未启动,也返回true |
terminate() | 终止线程的执行。线程可以立即终止,也可以不立即终止,这取决于操作系统的调用策略。在 terminate() 之后使用 QThread::wait() 来确保 |
finished | 当线程结束时会发出该信号,可以通过信号槽机制实现线程的清理工作 |
代码示例:新建线程获取实时时间,返回给主线程显示
- 设计 UI 界面
- 新建 Thread类,继承自 QThread
- 重写 run() 函数,声明信号
thread.h
class Thread : public QThread
{
Q_OBJECT
public:
Thread();
//重写线程入口函数
void run() override;
//自定义信号
signals:
void sendTime(QString time);
};
thread.cpp
void Thread::run()
{
while(1)
{
QString time = QDateTime::currentDateTime().toString("hh:mm:ss");//获取当前时间
emit Thread::sendTime(time);//发出信号
sleep(1);
}
}
- 编写按钮槽函数,声明线程对象
widget.h
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
//按钮的槽函数
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
Thread thread;//线程对象
};
widget.cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//捕获线程发出的信号
connect(&thread, &Thread::sendTime, this, [=](QString time){
ui->label->setText(time);
});
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_clicked()
{
thread.start();//开启线程
}
运行结果如下:
多线程 connect 参数补充
- Qt规定 UI 界面只能由主线程改变,线程函数内部不允许操作 UI 图形界面,一般用作数据处理
- connect() 函数第五个参数表示的为连接的方式,且只有在多线程的时候才有意义
connect() 函数原型如下:
第五个参数为Qt::ConnectionType,用于指定信号和槽的连接类型。同时影响信号的传递方式和槽函数的执行顺序。Qt::ConnectionType 提供方式如下:
方式 | 说明 |
---|---|
Qt::AutoConnection | 在 Qt 中,会根据信号和槽函数所在的线程自动选择连接类型。如果信号和槽函数在同一线程中,那么使用Qt::DirectConnection 类型;如果它们位于不同的线程中,那么使用 Qt::QueuedConnection 类型 |
Qt::DirectConnection | 当信号发出时,槽函数会立即在同一线程中执行。这种连接类型适用于信号和槽函数在同一线程中的情况,可以实现直接的函数调用,但需要注意线程安全 |
Qt::QueuedConnection | 当信号发出时,槽函数会被插入到接收对象所属的线程的事件队列中,等待下一次事件循环时执行。这种连接类型适用于信号和槽函数在不同线程中的情况,可以确保线程安全 |
Qt::BlockingQueuedConnection | 与 Qt::QueuedConnection 类似,但是发出信号的线程会被阻塞,直到槽函数执行完毕,这种连接类型适用于需要等待槽函数执行完毕再继续的场景,需要注意可能引起线程死锁 |
Qt::UniqueConnection | 这是一个标志,可以使用 位或 与上述任何一种连接类型组合使用 |
线程安全
实现线程互斥和同步常用的类有:
- 互斥锁:QMutex,QMutexLocker
- 条件变量:QWaitCondition
- 信号量:QSemaphore
- 读写锁:QReadLocker、QWriteLocker、QReadWriteLock
互斥锁
互斥锁是一种保护和防止多个线程同时访问同一对象实例的方法,在 Qt 中,互斥锁主要是通过 QMutex类
实现
- QMutex
特点:QMutex 是 Qt 框架提供的互斥锁类,用于保护共享资源的访问,实现线程间的互斥操作。
用途:在多线程环境下,通过互斥锁来控制对共享数据的访问,确保线程安全。
QMutex mutex;
mutex.lock();//加锁
//临界区访问共享资源
//......
mutex.unlock();//解锁
- QMutexLocker
特点:QMutexLocker 是 QMutex 的辅助类,也可以对互斥锁进行上锁和解锁操作。使用RAII 机制,防止因抛异常等操作,执行流跳转,互斥锁未解锁,导致其他线程死锁。原理是让互斥锁的上锁和解锁随对象的构造和析构
用途:简化对互斥锁的上锁和解锁操作,避免忘记解锁导致的死锁等问题
QMutex mutex;
{
//locker的生命周期随作用域
QMutexLocker locker(&mutex);//加锁随对象构造
//访问共享资源
//...
}//出作用域,locker析构解锁
- 读写锁
特点:
QReadWriteLock 是读写锁,用于控制读和写的并发访问
QReadLocker 用于读操作上锁,允许多个线程同时读取共享资源
QWriteLocker 用于写操作上锁,只允许一个线程写入共享资源
用途:在某些情况下,多个线程可以同时读取共享数据,但只有一个线程能够进行写操作。读写锁提供了更高效的并发访问方式:
QReadWriteLock rwlock;
//在读操作中使用读锁
{
QReadLocker locker(&rwlock);//上读锁
//读写共享资源
//...
}//作用域结束自动解锁
//在写操作中使用写锁
{
QWriteLocker locker(&rwlock);//上写锁
//修改共享资源
//...
}//作用域结束自动解锁
代码示例:多个线程对同一变量进行累加
- 创建 QWidget 项目,新建 Thread类,继承自 QThread
- 编写
thread.h
,声明 run() 函数 和 成员变量
class Thread : public QThread
{
Q_OBJECT
public:
Thread(QObject *parent = nullptr);
//线程启动函数
void run();
public:
static QMutex mutex;//多线程互斥锁
static int num;//多线程累加的变量
};
编写 thread.cpp
,此处不加互斥锁
//静态成员变量类外初始化
int Thread::num = 0;
QMutex Thread::mutex;
Thread::Thread(QObject *parent)
:QThread(parent)
{}
void Thread::run()
{
for(int i = 0; i < 50000; ++i)
++this->num;
}
- 编写
widget.cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//创建线程
Thread t1(this), t2(this);
//启动线程
t1.start();
t2.start();
//回收线程资源
t1.wait();
t2.wait();
qDebug() << Thread::num;
}
运行结果如下:
可以看到,两个线程都各自对 num 累加50000次,预期结果应该是 100000,但是因为多线程没有互斥访问共享变量,导致错误
- 多线程访问共享资源,加锁。编写
thread.cpp
void Thread::run()
{
for(int i = 0; i < 50000; ++i)
{
QMutexLocker locker(&this->mutex);
++this->num;
}
}
运行结果如下:
条件变量
条件变量是多线程编程中的一种同步机制,用于协调线程之间的执行顺序或状态变化。它允许线程在某些条件满足之前阻塞执行,并在条件满足后被通知并恢复执行。条件变量通常与互斥锁配合使用,以确保在访问共享资源时的线程安全。
Qt 中,QWaitCondition 实现上述机制
特点:QWaitCondition 是 Qt 框架提供的条件变量类,用于线程之间的消息通信和同步
用途:在某个条件满足时等待或唤醒线程,用于线程的同步和协调
QMutex mutex;
QWaitCondition cond;
//加锁
mutex.lock();
//检查条件是否满足,若不满足则等待
while(!....)//循环判断是否满足条件
{
//不满足则等待条件满足,并释放锁
cond.wait(&mutex);
}
//到此是条件满足
//...执行逻辑
//唤醒其他在该条件变量上等待的线程,再解锁
cond.wakeAll();
mutex.unlock();
信号量
信号量是一种用于进程或线程间同步和互斥的机制。它可以控制对共享资源的访问,防止出现竞争条件。信号量主要分为两种类型:计数信号量和二值信号量(或互斥量)。计数信号量可以允许多个线程访问共享资源,而二值信号量则仅允许一个线程访问。
信号量类似增强的互斥锁,不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量
特点:QSemaphore 是 Qt 框架提供的计数信号量,用于控制同时访问共享资源的线程数量
用途:限制并发线程数量,用于解决一些资源有限的问题
QSemaphore sem(2);//同时允许两个线程访问共享资源
//在需要访问共享资源的线程中
sem.acquire();//尝试获取信号量,若满则阻塞
//访问共享资源
//...
sem.release();//释放信号量
结束语
感谢你的阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。