在Qt中使用线程可以粗略的分为三种方式:由Qt API提供的一般线程的实现方式(QThread
、QObject
)、线程池(QRunnable
、QtConcurrent
)、c++ 11提供的线程(c++std::thread
、std::sync
)。这里我将对每一种实现方式以实例Demo详细的展开,所有Demo均采用Qt 6.3
版本,如和我不是相同的版本请查询Qt文档,是否支持相应的接口。请注意某些接口要求采用c++ 17,这里我统一采用c++ 17使用时请大家关注文档需求。为了方便大家直接拷贝运行,所有示例均采用单一文件的方式。
#include
#include
#include
#include
class ThreadTest : public QThread
{
Q_OBJECT
public:
explicit ThreadTest(QObject *parent = nullptr) : QThread(parent){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
virtual ~ThreadTest(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
protected:
//线程入口
void run() override
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
};
int main(int argc, char *argv[])
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
QCoreApplication a(argc, argv);
ThreadTest t;
//启动线程
t.start();
//2秒钟后关闭线程和主程序
QTimer::singleShot(2000,[&t,&a](){
t.quit();
t.wait();
a.quit();
});
return a.exec();
}
#include "main.moc"
执行结果:
0x3074 int main(int, char**)
0x3074 ThreadTest::ThreadTest(QObject*)
0x386c virtual void ThreadTest::run()
0x3074 virtual ThreadTest::~ThreadTest()
阐述:
这里使用继承QThread重写run函数的方式来实现多线程,可以看到该对象的构造和析构均在主线程中执行,run函数在子线程中执行。
请注意程序末尾的#include "main.moc"
,若没有该语句会报如下错误:
AutoMoc error
-------------
"SRC:/main.cxx"
contains a "Q_OBJECT" macro, but does not include "main.moc"!
Consider to
- add #include "main.moc"
- enable SKIP_AUTOMOC for this file
因为这里继承自QThread并添加了Q_OBJECT
宏定义,且没有头文件所以需要添加该语句来跳过AUTOMOC
的执行。如果你不需要使用信号槽,可去除Q_OBJECT
宏定义,则可以不用引用该文件。
void run() override
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO << "start";
int count = 0;
while (true) {
qDebug() << "count=" << ++count;
QThread::sleep(1);
}
qDebug() << "end.";
}
输出结果:
0x3c44 int main(int, char**)
0x3c44 ThreadTest::ThreadTest(QObject*)
0x1c4c virtual void ThreadTest::run() start
count= 1
count= 2
count= 3
count= 4
count= 5
...
阐述:你会发现原本2秒中之后就该关闭的程序结果一直在输出,只要你不强行结束那它便会一直执行下去。大部分新手都误以为调用quit
函数后线程就会自动结束,实际结果令他大失所望百思不得其解。于是部分人查看文档或查找资料采用如下的方式来结束线程:
QTimer::singleShot(2000,[&t,&a](){
// t.quit();
// t.wait();
t.terminate();
a.quit();
});
执行结果:
0x6e0 int main(int, char**)
0x6e0 ThreadTest::ThreadTest(QObject*)
0x3510 virtual void ThreadTest::run() start
count= 1
count= 2
0x6e0 virtual ThreadTest::~ThreadTest()
阐述:这样一改发现程序执行达到预期的结果,堪称完美。但是我们看Qt官方是如何描述该接口的:
Warning: This function is dangerous and its use is discouraged. The thread can be terminated at any point in its code path. Threads can be terminated while modifying data. There is no chance for the thread to clean up after itself, unlock any held mutexes, etc. In short, use this function only if absolutely necessary.
意思是该接口很危险如非必要请不要使用,因为线程可能会在任何地方结束,来不及自行清理释放资源或解锁互斥量等操作。换句话来说就是Qt希望线程在正确的位置去结束,而不是这种强行打断的方式。
正确的做法:
...
void run() override
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO << "start";
int count = 0;
while (!QThread::currentThread()->isInterruptionRequested()) {
qDebug() << "count=" << ++count;
QThread::sleep(1);
}
qDebug() << "end.";
}
...
QTimer::singleShot(2000,[&t,&a](){
t.requestInterruption();
t.quit();
t.wait();
a.quit();
});
执行结果:
0x2aa4 int main(int, char**)
0x2aa4 ThreadTest::ThreadTest(QObject*)
0x2128 virtual void ThreadTest::run() start
count= 1
count= 2
end.
0x2aa4 virtual ThreadTest::~ThreadTest()
阐述:
这里我们只改了两个地方,第一个是把死循环条件while(true)
改为了while (!QThread::currentThread()->isInterruptionRequested())
,第二个是在结束的时候添加了t.requestInterruption();
函数。目的是为了在退出前先打断这个死循环,然后调用quit
去退出消息循环,最后调用wait
等待线程结束。
聪明的你肯定还发现了输出结果有点儿不一样,现在多输出了个end.
,之前调用terminate
的时候是没有的。那是因为强行结束时只能在循环里面,强行终止后并不会继续往下执行。
假设我们用于计数的count
是new
出来的,在end
的位置去释放资源。倘若调用terminate
强行结束,那这个count
就不会被释放造成内存泄漏。
requestInterruption
是Qt提供的线程打断函数且是线程安全的但需配合isInterruptionRequested
一起使用。
这里建议如非必要尽量少用死循环,部分功能完全可以使用定时器或消息循环来实现。
void run() override
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO << "start";
QTimer::singleShot(1000,[](){
qDebug() << QThread::currentThreadId() << "线程内部定时器";
});
}
...
QTimer::singleShot(2000,[&t,&a](){
t.quit();
t.wait();
a.quit();
});
输出结果:
0x2214 int main(int, char**)
0x2214 ThreadTest::ThreadTest(QObject*)
0x3cfc virtual void ThreadTest::run() start
0x2214 virtual ThreadTest::~ThreadTest()
阐述:
发现在线程内部本应该1秒后的执行并没有输出打印结果。我们的主程序是在2秒后结束的,完全有时间等子线程执行打印函数。这是因为run函数执行结束后QTimer就立即被析构了,没有机会执行定时任务。
正确的做法:
void run() override
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO << "start";
QTimer::singleShot(1000,[](){
qDebug() << QThread::currentThreadId() << "线程内部定时器";
});
exec();
}
执行结果:
0x4228 int main(int, char**)
0x4228 ThreadTest::ThreadTest(QObject*)
0x3cc4 virtual void ThreadTest::run() start
0x3cc4 线程内部定时器
0x4228 virtual ThreadTest::~ThreadTest()
阐述:在run函数结束时添加exec();
进入消息循环,发现定时器任务已经可以正常的执行了。exec
内部也是一个死循环,不过这属于Qt内部的消息循环,作用和main.cxx
中的return a.exec();
一样的。
#include
#include
#include
#include
#include
#include
#include
class ThreadTest : public QThread
{
Q_OBJECT
public:
explicit ThreadTest(QObject *parent = nullptr) : QThread(parent){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
virtual ~ThreadTest(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
protected:
//线程入口
void run() override
{
//新建socket,注意这里不能有父类
QTcpSocket *socket = new QTcpSocket;
//禁用代理
socket->setProxy(QNetworkProxy::NoProxy);
//状态改变
connect(socket,&QTcpSocket::stateChanged,this,&ThreadTest::stateChanged);
//接收到数据
connect(socket,&QTcpSocket::readyRead,socket,std::bind(&ThreadTest::recvData,this,socket));
//连接服务器
socket->connectToHost(QHostAddress("127.0.0.1"),8000);
//开启消息循环
exec();
//关闭socket
socket->close();
delete socket;
socket = nullptr;
qDebug() << QThread::currentThreadId() << "线程结束";
}
//状态改变
void stateChanged(QAbstractSocket::SocketState state)
{
qDebug() << QThread::currentThreadId() << "state:" << state;
}
//接收到数据
void recvData(QTcpSocket *socket)
{
QByteArray byte = socket->readAll();
qDebug().noquote() << QThread::currentThreadId() << "recv:" << byte;
socket->write(byte);
}
};
int main(int argc, char *argv[])
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
QCoreApplication a(argc, argv);
ThreadTest t;
//启动线程
t.start();
//5秒钟后关闭线程和主程序
QTimer::singleShot(5000,[&t,&a](){
t.quit();
t.wait();
a.quit();
});
return a.exec();
}
#include "main.moc"
执行结果:
0x453c int main(int, char**)
0x453c ThreadTest::ThreadTest(QObject*)
0x453c state: QAbstractSocket::HostLookupState
0x453c state: QAbstractSocket::ConnectingState
0x453c state: QAbstractSocket::ConnectedState
0x2a04 recv: 你好,世界
0x2a04 recv: 你好,世界
0x2a04 recv: 你好,世界
0x2a04 recv: 你好,世界
0x2a04 recv: 你好,世界
0x2a04 线程结束
0x453c virtual ThreadTest::~ThreadTest()
阐述:
这里为了正常的测试还把程序的结束时间延迟到5秒之后。TCP SOCKET能正常的异步(同步收发在此不作讨论)收发消息,说明内部的消息循环是成功执行的。在调用exec
接口后,线程会开启消息循环并阻塞到这个地方。在5秒之后调用quit
函数退出消息循环后,程序继续往下执行socket的关闭和资源释放的工作。
不知道从何时开始起,网上充斥着继承自QThread的对象不能使用信号槽连接的说法。可以看到,在如上的程序中我使用了两个信号槽连接分别对应了两种不同的结果,如果你对这种结果感到意外的话,说明你对于信号槽的链接这块的知识掌握还不太牢固,知其然不知其所以然。
一般大家都会说,信号的发送者和信号的接收者如果处于相同线程则槽函数在发送者线程执行;信号的发送者和信号的接收者处于不同线程则槽函数在接收者线程执行。这样也不能说不对,只是有些限定条件,这仅适用于信号槽的链接类型是自动判断的情况下。
由于篇幅有限关于信号槽的链接暂且不作详细说明,有兴趣的同学请留言我另起文档再做阐述。
第一种信号槽链接的接收者是this,即对象本身。根据说明文档,QThread只有run函数在子线程中执行,所以this对象其实是在主线程中执行的(前提是没有去移动线程)。这也就说明了stateChanged
函数为什么是执行在主线程的。
第二种信号槽链接的接收者是socket
,socket
对象是我们在子线程中实例化的,所以对应了recvData
函数在子线程中执行。这里我是传入了一个QTcpSocket
对象,大家使用的时候可以将其设置为成员变量,这样就不用传递该参数。
现在如果我们要让stateChanged
函数在子线程中执行那怎么办呢?
a. 更改链接类型为直连类型
connect(socket,&QTcpSocket::stateChanged,this,&ThreadTest::stateChanged,Qt::DirectConnection);
执行结果:
0x179c int main(int, char**)
0x179c ThreadTest::ThreadTest(QObject*)
0x1a30 state: QAbstractSocket::HostLookupState
0x1a30 state: QAbstractSocket::ConnectingState
0x1a30 state: QAbstractSocket::ConnectedState
0x1a30 recv: 你好,世界
0x1a30 state: QAbstractSocket::ClosingState
0x1a30 state: QAbstractSocket::UnconnectedState
0x1a30 线程结束
0x179c virtual ThreadTest::~ThreadTest()
阐述:只需在connect
接口添加第五个参数为Qt::DirectConnection
,槽函数的执行相当于函数调用,自然和信号发送者处于同一个线程。
b. 将信号接收者更改为和信号发送者处于同一线程的对象
connect(socket,&QTcpSocket::stateChanged,socket,std::bind(&ThreadTest::stateChanged,this,std::placeholders::_1));
阐述:这里更改了信号的接收者,使得与发送者处于同一个线程中。因为stateChanged
函数不是socket
的成员函数,所以没法直接进行连接。这里使用std::bind去适配该参数,同理使用lambda
函数来实现也是一样的。
所以结论是在run函数中进行信号槽的链接完全是没有问题的。该方式创建子线程很方便,但是灵活性不高。
#include
#include
#include
#include
#include
#include
#include
class ThreadTest {
public:
explicit ThreadTest(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
m_t = QThread::create(&ThreadTest::task,this);
m_t->start();
};
~ThreadTest(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
m_t->quit();
m_t->wait();
delete m_t;
m_t = nullptr;
}
protected:
void task()
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
private:
QThread *m_t{};
};
int main(int argc, char *argv[])
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
QCoreApplication a(argc, argv);
ThreadTest t;
//2秒钟后关闭线程和主程序
QTimer::singleShot(2000,[&a](){
a.quit();
});
return a.exec();
}
执行结果:
0x4184 int main(int, char**)
0x4184 ThreadTest::ThreadTest()
0x36b4 void ThreadTest::task()
0x4184 ThreadTest::~ThreadTest()
阐述:这也是我比较喜欢的一种方式,因为它比继承QThread还要方便,直接将一个函数转移到子线程中去执行。这个有点儿类似于QtConcurrent
线程池和std::thread
、std::sync
的使用方式。缺点是它不能调用QThread的内部的exec开启消息循环,对于必须要执行消息循环的对象来说还是有点儿不太方便。不过可以自行实现消息循环来达到相同的目的。
这里还是以TCP SOCKET为例:
#include
#include
#include
#include
#include
#include
#include
class ThreadTest {
public:
explicit ThreadTest(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
m_t = QThread::create(&ThreadTest::task,this);
m_t->start();
};
~ThreadTest(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
m_t->quit();
m_t->wait();
delete m_t;
m_t = nullptr;
}
protected:
void task()
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
//创建一个socket,请注意这里不能有父类
QTcpSocket *socket = new QTcpSocket;
socket->setProxy(QNetworkProxy::NoProxy);
//socket链接状态改变
QObject::connect(socket,&QTcpSocket::stateChanged,socket,[socket](QAbstractSocket::SocketState state){
qDebug() << QThread::currentThreadId() << "state:" << state;
});
//socket接收到数据
QObject::connect(socket,&QTcpSocket::readyRead,socket,[socket](){
QByteArray byte = socket->readAll();
qDebug() << QThread::currentThreadId() << "recv:" << byte;
socket->write(byte);
});
//链接服务器
socket->connectToHost(QHostAddress("127.0.0.1"),8000);
QEventLoop loop;
QObject::connect(QThread::currentThread(),&QThread::finished,&loop,&QEventLoop::quit);
loop.exec();
socket->close();
delete socket;
socket = nullptr;
qDebug() << QThread::currentThreadId() << "线程结束";
}
private:
QThread *m_t{};
};
int main(int argc, char *argv[])
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
QCoreApplication a(argc, argv);
ThreadTest t;
//3秒钟后关闭线程和主程序
QTimer::singleShot(3000,[&a](){
a.quit();
});
return a.exec();
}
执行结果:
0x1ab8 int main(int, char**)
0x1ab8 ThreadTest::ThreadTest()
0x3f6c void ThreadTest::task()
0x3f6c state: QAbstractSocket::HostLookupState
0x3f6c state: QAbstractSocket::ConnectingState
0x3f6c state: QAbstractSocket::ConnectedState
0x3f6c recv: "hello world."
0x3f6c recv: "hello world."
0x3f6c recv: "hello world."
0x1ab8 ThreadTest::~ThreadTest()
0x3f6c state: QAbstractSocket::ClosingState
0x3f6c state: QAbstractSocket::UnconnectedState
0x3f6c 线程结束
阐述:
可以看到socket正常的异步收发数据,这里使用QEventLoop
来实现自己的消息循环,在程序退出的时候结束消息循环然后释放资源。
该方式是目前最灵活的方式,也是目前Qt主推的一种方式,但该方式实现相对复杂一些。这里同样以TCP SOCKET为例:
#include
#include
#include
#include
#include
#include
#include
class TcpSocket : public QObject {
Q_OBJECT
public:
explicit TcpSocket(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
virtual ~TcpSocket() {
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
public slots:
void start()
{
//新建socket,注意不能有父类
m_socket = new QTcpSocket;
//禁用代理
m_socket->setProxy(QNetworkProxy::NoProxy);
//链接状态改变
connect(m_socket,&QTcpSocket::stateChanged,this,&TcpSocket::stateChanged);
//接收到数据
connect(m_socket,&QTcpSocket::readyRead,this,&TcpSocket::recvData);
//链接到服务器
m_socket->connectToHost(QHostAddress("127.0.0.1"),8000);
}
void stop()
{
m_socket->close();
delete m_socket;
m_socket = nullptr;
}
void stateChanged(QAbstractSocket::SocketState state) {
qDebug() << QThread::currentThreadId() << "state:" << state;
}
void recvData()
{
QByteArray byte = m_socket->readAll();
qDebug() << QThread::currentThreadId() << "recv:" << byte;
m_socket->write(byte);
}
private:
QTcpSocket *m_socket{};
};
int main(int argc, char *argv[])
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
QCoreApplication a(argc, argv);
TcpSocket socket;
QThread t;
QObject::connect(&t,&QThread::started,&socket,&TcpSocket::start);
QObject::connect(&t,&QThread::finished,&socket,&TcpSocket::stop);
socket.moveToThread(&t);
t.start();
//3秒钟后关闭线程和主程序
QTimer::singleShot(3000,[&a,&t](){
t.quit();
t.wait();
a.quit();
});
return a.exec();
}
#include "main.moc"
执行结果:
0x44d4 int main(int, char**)
0x44d4 TcpSocket::TcpSocket()
0x2850 state: QAbstractSocket::HostLookupState
0x2850 state: QAbstractSocket::ConnectingState
0x2850 state: QAbstractSocket::ConnectedState
0x2850 recv: "hello world."
0x2850 recv: "hello world."
0x2850 state: QAbstractSocket::ClosingState
0x2850 state: QAbstractSocket::UnconnectedState
0x44d4 virtual TcpSocket::~TcpSocket()
阐述:使用该方式请注意线程的正确退出和资源释放的问题。
#include
#include
#include
#include
#include
#include
#include
#include
#include
class Task : public QRunnable {
public:
explicit Task(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
virtual ~Task(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
protected:
void run() override{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
};
int main(int argc, char *argv[])
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
QCoreApplication a(argc, argv);
Task *task = new Task;
task->setAutoDelete(true); //设置任务执行完毕自动析构
QThreadPool::globalInstance()->start(task);
//3秒钟后关闭线程和主程序
QTimer::singleShot(3000,[&a](){
//等待线程池执行结束
QThreadPool::globalInstance()->waitForDone();
a.quit();
});
return a.exec();
}
执行结果:
0x43f8 int main(int, char**)
0x43f8 Task::Task()
0x42c0 virtual void Task::run()
0x42c0 virtual Task::~Task()
阐述:
继承QRunnable
重写run
函数,使用QThreadPool
去启动一个线程池。这里我使用了全局线程池,大家可以自行创建一个线程池并设置好最大线程数。线程池相对于直接实例化一个线程来说效率更高更加节省资源。
QRunnable
并非继承自QObject
所以不能实现信号槽的链接,如需使用信号槽请继承QObject
且必须写在所有继承之前,并声明Q_OBJECT
宏。
class Task : public QObject,public QRunnable {
Q_OBJECT
public:
explicit Task(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
virtual ~Task(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
protected:
void run() override{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
};
void run(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
int main(int argc, char *argv[])
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
QCoreApplication a(argc, argv);
QRunnable *task = QRunnable::create(run);
task->setAutoDelete(true);
QThreadPool::globalInstance()->start(task);
//3秒钟后关闭线程和主程序
QTimer::singleShot(3000,[&a](){
//等待线程池执行结束
QThreadPool::globalInstance()->waitForDone();
a.quit();
});
return a.exec();
}
#include
#include
#include
#include
#include
#include
void run(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
int main(int argc, char *argv[])
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
QCoreApplication a(argc, argv);
QtConcurrent::run(run);
//3秒钟后关闭线程和主程序
QTimer::singleShot(3000,[&a](){
//等待线程池执行结束
QThreadPool::globalInstance()->waitForDone();
a.quit();
});
return a.exec();
}
#include
#include
#include
#include
void run(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
int main(int argc, char *argv[])
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
QCoreApplication a(argc, argv);
std::thread t(run);
//3秒钟后关闭线程和主程序
QTimer::singleShot(3000,[&a,&t](){
t.join();
a.quit();
});
return a.exec();
}
阐述:
线程退出时必须调用join
来等待线程结束,或调用detech
分离线程。
#include
#include
#include
#include
void run(){
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
}
int main(int argc, char *argv[])
{
qDebug() << QThread::currentThreadId() << Q_FUNC_INFO;
QCoreApplication a(argc, argv);
std::async(std::launch::async,run);
//3秒钟后关闭线程和主程序
QTimer::singleShot(3000,[&a](){
a.quit();
});
return a.exec();
}
阐述:
后面几个同样可以创建Qt的消息循环,使用方式同之前的一样。
因时间原因,并没有太过详细的阐述,如有需要请留言反馈。