QT中子线程和多线程的使用记录档
子线程
在QT中将子类中的运算扔到子线程中有两种方法,一种是将子类继承于QObject,调用QObject::moveToThread()方法,从主线程中发送信号调用子类的槽函数;另一种是将子类继承于QThread,重写run()函数。
继承于QObject
子类定义
//.h
class MyChildQObjectThread : public QObject
{
Q_OBJECT
public:
MyChildQObjectThread();
~MyChildQObjectThread();
public slots:
void slot_perform();
signals:
void sig_begin_perform();
void sig_finish_perform();
};
//.cpp
MyChildQObjectThread::MyChildQObjectThread()
{
qDebug()<<"MyChildQObjectThread的构造函数所在线程:"<<QThread::currentThreadId();
connect(this,&MyChildQObjectThread::sig_begin_perform,this,&MyChildQObjectThread::slot_perform);
}
MyChildQObjectThread::~MyChildQObjectThread()
{
qDebug()<<"MyChildQObjectThread的析构函数所在线程:"<<QThread::currentThreadId();
}
void MyChildQObjectThread::slot_perform()
{
qDebug()<<"MyChildQObjectThread的执行函数所在线程:"<<QThread::currentThreadId();
emit sig_finish_perform();
}
应用
//.h
QThread WorkThread;
MyChildThread *pMyChildThread;
//.cpp
pMyChildQObjectThread = new MyChildQObjectThread();
pMyChildQObjectThread->moveToThread(&WorkThread);
connect(&WorkThread,&QThread::finished,pMyChildQObjectThread,&QObject::deleteLater);
connect(pMyChildQObjectThread,&MyChildQObjectThread::sig_finish_perform,this,&MainWindow::slot_receive_perform);
WorkThread.start();
emit pMyChildQObjectThread->sig_begin_perform();
运行结果
MainWindow 所在线程: 0x658
MyChildQObjectThread的构造函数所在线程: 0x658
MyChildQObjectThread的执行函数所在线程: 0x7204
MyChildQObjectThread的析构函数所在线程: 0x7204
总结
- 当子类继承于QObject时,需要定义一个QThread线程用来调用QObject::moveToThread()方法。
- 子类的构造函数是在主线程中执行,槽函数及子类析构函数均在子线程中执行。
- 子类的析构,需要通过手动释放定义的QThread,通过QThread::finished()信号来调用QObject::deleteLater(),以此进入析构函数;不然定义的QThread不会自动结束线程。
继承于QThread
//.h
class MyChildThread : public QThread
{
Q_OBJECT
public:
MyChildThread();
~MyChildThread();
protected:
void run() override;
signals:
};
//.cpp
MyChildThread::MyChildThread()
{
qDebug()<<"MyChildThread的构造函数所在线程:"<<QThread::currentThreadId();
}
MyChildThread::~MyChildThread()
{
qDebug()<<"MyChildThread的析构函数所在线程:"<<QThread::currentThreadId();
}
void MyChildThread::run()
{
qDebug()<<"MyChildThread的执行函数所在线程:"<<QThread::currentThreadId();
}
应用
pMyChildThread = new MyChildThread();
connect(pMyChildThread,&QThread::finished,pMyChildThread,&QObject::deleteLater);
pMyChildThread->start();
运行结果
MainWindow 所在线程: 0x6968
MyChildThread的构造函数所在线程: 0x6968
MyChildThread的执行函数所在线程: 0x1694
MyChildThread的析构函数所在线程: 0x6968
总结
- 当子类继承于QThread时,需要重写run()函数来执行想要在子线程中执行的操作。
- 子类的构造函数和析构函数均在主线程中执行,run()函数在子线程中执行。
- run()函数执行完毕之后线程即销毁,在finished()信号与deleteLater()连接后执行析构函数。
线程池
以下是QT帮助文档中对线程池的描述
QThreadPool管理和回收单独的QThread对象,以帮助减少使用线程的程序中的线程创建成本。每个Qt应用程序都有一个全局QThreadPool对象,可以通过调用globalInstance()来访问它。
要使用一个QThreadPool线程,子类QRunnable并实现run()虚函数。然后创建该类的一个对象并将其传递给QThreadPool::start()。
从上述描述中,我们可以得知,在程序运行过程中对线程的创建和销毁是需要耗费资源的,因此当在需要频繁创建&销毁子线程的场景中,使用QThreadPool是个不错的选择。
子类定义
//.h
struct MyStruct{
int x;
QString str;
};
class MyThreadPool : public QObject, public QRunnable
{
Q_OBJECT
public:
MyThreadPool(MyStruct temp);
~MyThreadPool();
protected:
void run() override;
private:
MyStruct m_struct;
QElapsedTimer test_timer;
signals:
void sig_output(MyStruct result);
};
//.cpp
MyThreadPool::MyThreadPool(MyStruct temp) : m_struct(temp)
{
qDebug()<<"MyThreadPool的构造函数所在线程:"<<QThread::currentThreadId();
}
MyThreadPool::~MyThreadPool()
{
qDebug()<<"MyThreadPool的析构函数所在线程:"<<QThread::currentThreadId();
}
void MyThreadPool::run()
{
test_timer.restart();
while(test_timer.elapsed() < 100)
;
int id = (int)(QThread::currentThreadId());
m_struct.str.append(tr("第%1次线程ID:0x").arg(m_struct.x));
m_struct.str.append(QString::number(id,16));
emit sig_output(m_struct);
}
应用
{
p_my_threadpool = new MyThreadPool(mystruct);
connect(p_my_threadpool,&MyThreadPool::sig_output,this,&MainWindow::slot_receive_result);
p_my_threadpool->setAutoDelete(m_auto_delete_runnable);
QThreadPool::globalInstance()->start(p_my_threadpool);
}
void MainWindow::slot_receive_result(MyStruct temp)
{
ui->textEdit->append(QString("接收到处理结果:%1\n").arg(temp.x).append(temp.str));
if(temp.x == timer_max_count)
{
ui->textEdit->append(str);
}
}
运行结果
MainWindow 所在线程: 0x2c3c
"MyThreadPool第1次的构造函数所在线程:" 0x2c3c
"MyThreadPool第2次的构造函数所在线程:" 0x2c3c
"MyThreadPool第3次的构造函数所在线程:" 0x2c3c
"MyThreadPool第4次的构造函数所在线程:" 0x2c3c
"MyThreadPool第5次的构造函数所在线程:" 0x2c3c
"MyThreadPool第6次的构造函数所在线程:" 0x2c3c
"MyThreadPool第7次的构造函数所在线程:" 0x2c3c
"MyThreadPool第8次的构造函数所在线程:" 0x2c3c
"MyThreadPool第9次的构造函数所在线程:" 0x2c3c
"MyThreadPool第1次的析构函数所在线程:" 0x6fe0
"MyThreadPool第10次的构造函数所在线程:" 0x2c3c
"MyThreadPool第2次的析构函数所在线程:" 0x4a3c
"MyThreadPool第3次的析构函数所在线程:" 0x1fd8
"MyThreadPool第4次的析构函数所在线程:" 0x59d4
"MyThreadPool第5次的析构函数所在线程:" 0x660c
"MyThreadPool第6次的析构函数所在线程:" 0x54e0
"MyThreadPool第7次的析构函数所在线程:" 0x6b10
"MyThreadPool第8次的析构函数所在线程:" 0x56bc
"MyThreadPool第9次的析构函数所在线程:" 0x445c
"MyThreadPool第10次的析构函数所在线程:" 0x6fe0
定时器1次结束计时
定时器2次结束计时
定时器3次结束计时
定时器4次结束计时
定时器5次结束计时
定时器6次结束计时
定时器7次结束计时
定时器8次结束计时
定时器9次结束计时
定时器10次结束计时
接收到处理结果:1
第1次线程ID:0x6fe0
接收到处理结果:2
第2次线程ID:0x4a3c
接收到处理结果:3
第3次线程ID:0x1fd8
接收到处理结果:4
第4次线程ID:0x59d4
接收到处理结果:5
第5次线程ID:0x660c
接收到处理结果:6
第6次线程ID:0x54e0
接收到处理结果:7
第7次线程ID:0x6b10
接收到处理结果:8
第8次线程ID:0x56bc
接收到处理结果:9
第9次线程ID:0x445c
接收到处理结果:10
第10次线程ID:0x6fe0
总结
- 线程池的任务类需要重写run()函数来执行想要扔到子线程的耗时操作。
- 线程池的任务构造函数在主线程中执行,run()函数和析构函数均在子线程中执行。
- 默认情况下,在最后一个线程退出run()函数后,QThreadPool::autoDelete会删除任务类;
- 在多次重复调用相同任务类的场景下,启用autoDelete会创造一个竞态条件,因此推荐用QRunnable::setAutoDelete()禁用自动删除任务类。
- 线程池的子线程并不是在调用QThreadPool::globalInstance()->start()后立即执行,而是将任务类扔到一个任务队列中,按队列顺序执行;可以更改start()的第二个参数priority来控制该任务类的执行顺序。
- 由上述总结可以得出,线程池的结果输出顺序和任务类启用顺序不一致,因此需要注意在要求顺序输出的场景下进行额外操作。
并发线程类
2022.12.21日更新
Qt Concurrent类可以将一个函数扔到子线程中去执行,根据Qt文档的说明,它封装了高级API,避免使用互斥锁、读写锁、信号量这些低级线程用语。
我的个人使用示例如下:
- 在pro文件中包含
QT += concurrent
- 在相应头文件中包含
#include <QtConcurrent>
- 多数情况下,我们想在子线程中运行的函数应该是非静态的成员函数(就我个人情况而言),以MainWindow内函数为例:
void MainWindow::example_function(int input_A, int input_B)
{}
void MainWindow::run_Concurrent()
{
int input_x, input_y;
input_x = 1;
input_y = 2;
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
QFuture<void> result = QtConcurrent::run(&MainWindow::example_function,this,input_x,input_y);
#else
QFuture<void> result = QtConcurrent::run(this,&MainWindow::example_function,input_x,input_y);
#endif
result.waitForFinished();
}
2022.12.21日更新完毕
线程间通信
不同线程中进行数据通信主要有两种方式:共享内存和信号槽。
共享内存
共享内存想起来后补充
信号槽
数据类型
信号槽默认可以传输QT自己的元数据类型,如QString、double、int等类型,若想要传输自定义结构体数据,则需要在定义一个结构体后调用Q_DECLARE_METATYPE来向QT注册自定义结构体,如下:
//.h
struct MyStruct{
int x;
QString str;
};
Q_DECLARE_METATYPE(MyStruct)
signals:
void sig_output(MyStruct result);
//.cpp
connect(p_my_threadpool,&MyThreadPool::sig_output,this,&MainWindow::slot_receive_result);
connect的第五个参数
一般来说,我们在调用connect函数时只用了四个参数,即发送者,发送信号,接收者,接收槽函数,但是connect函数还存在了第五个参数:连接方式Qt::ConnectionType
连接类型 | 使用场景 |
---|---|
Qt::AutoConnection | 默认值,如果发送者和接收者在同一个线程,则执行Qt::DirectConnection,否则会执行Qt::QueuedConnection。 |
Qt::DirectConnection | 信号发出后,立即执行槽函数;槽函数在发送者线程执行。 |
Qt::QueuedConnection | 信号发出后,不会立即执行槽函数,而是等到接收者进入事件循环之后,槽函数才会被调用;槽函数在接收者线程执行。 |
Qt::BlockingQueuedConnection | 槽函数执行方式和Qt::QueuedConnection相同,不同之处在于该信号发出后,会阻塞发送者线程,直到槽函数执行完毕返回,因此若发送者和接收者在同一个线程中,将会造成死锁。此方式只能在多线程中运用。 |
Qt::UniqueConnection | 此连接方式在禁止多次连接相同信号和槽的时候启用,因为在信号和槽已经connect情况下再次连接时,connect将会失败。 |
Qt::SingleShotConnection | 此连接方式在只需要连接一次信号槽的时候启用,因为在信号发送一次后,connect将被断开,槽函数只会执行一次。 |
一般情况下,不需要设置第五个参数,QT会根据发送者和接收者所在的线程自行判断是用直接连接还是队列连接。
结语
以上是我在实际运用过程中的一点总结,以备后续查阅。
见识浅薄,以娱大家。
这是本项目的Github地址:ThreadPoolDemo
本项目编译环境:Qt 6.2.4 Community \ MSVC 2019