文章目录
线程相关
线程类
Qt线程三种使用:
QThread
a.继承QThread
重写QThread的run()函数,由于QThread继承QObject,故可以通过信号槽方式与主线程通信,再调用start启动线程。
QObject的moveToThread
void moveToThread(QThread *thread);将QObject类任务交给QThread来访问。
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = 0);
~Widget();
private:
Ui::Widget *ui;
};
class CalHelper : public QObject
{
Q_OBJECT
public:
explicit CalHelper(QObject *parent);
~CalHelper();
public slots:
void slotHandTimeOut();
private:
QTimer m_task_timer;
};
//cpp
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
QThread *thread = new QThread(this);
qDebug()<<"m_Thread parent"<<thread->parent();
CalHelper *task = new CalHelper(nullptr);//必须为空,有父对象的无法移动
QThread::sleep(1);
task->moveToThread(thread);
connect(thread, &QThread::finished, task, &QObject::deleteLater);
thread->start();
}
Widget::~Widget()
{
delete ui;
}
CalHelper::CalHelper(QObject *parent):QObject(parent)
{
qDebug()<<"timeout currentThreadId:"<<QThread::currentThreadId();
m_task_timer.start(30);
connect(&m_task_timer,&QTimer::timeout,this,&CalHelper::slotHandTimeOut);
}
CalHelper::~CalHelper()
{
qDebug()<<"CalHelper is delete";
}
void CalHelper::slotHandTimeOut()
{
qDebug()<<"timeout currentThreadId:"<<QThread::currentThreadId();
qDebug()<<"CalHelper parent"<<parent();
}
输出为:
main currentThreadId: 0x4f7c
thread parent Widget(0x74fdec, name = "Widget")
timeout currentThreadId: 0x4f7c
timeout currentThreadId: 0x1d80
CalHelper parent QObject(0x0)
timeout currentThreadId: 0x1d80
CalHelper parent QObject(0x0)
可见,未移动前,超时槽触发是在主线程,移动后,超时槽触发在子线程。moveToThread使得执行流在子线程执行。
使用moveToThread注意:
- 构造成功Object后,通过moveToThread()将Object对象移到到新线程中,如此一来obj执行流都将在子线程中运行,**但被移动obj即task的控制权仍然属于主线程。**movetoThread()的作用是将槽函数在指定的线程中调用。仅有槽函数在指定线程中调用,包括构造函数都仍然在主线程中调用!!!
- CalHelper须继承自顶层父类 Object,且构造时不能指定父对象,否则不能移动。
- 如果 Thread为nullptr,则该对象及其子对象的所有事件处理都将停止,因为它们不再与任何线程关联。
- 调用movetoThread() 时,移动对象的所有计时器将被重置。 计时器首先在当前线程中停止,然后在targetThread中重新启动(以相同的间隔),这时定时器属于子线程。若在线程之间不断移动对象可能会无限期地延迟计时器事件。
a适合高频任务;b适合单次任务。通过队列连接方式,使得在子线程执行,触发一次,执行一次。
常见接口:QThread *currentThread();//返回线程指针
Qt::HANDLE currentThreadId();//返回线程id。
1、当QObject接收到信号或发布的事件时,槽函数或事件处理程序将在对象所在的线程中运行。(如果QObject没有线程关联(即如果thread()返回nullptr),或者如果它位于没有运行事件循环的线程中,则它无法接收信号或发布的事件)。
2、默认情况下,QObject存在于创建它的线程中。可以使用thread()查询对象的线程关联,并使用moveToThread()更改对象的线程关联。
3、所有QObject必须与其父对象位于同一线程中。因此:如果所涉及的两个QObject位于不同的线程中,setParent()将失败。当一个QObject对象被移动到另一个线程时,该对象的所有子线程也将被自动移动。如果QObject对象有父对象,moveToThread()将失败。如果QObject对象是在QThread::run()中创建的,则它们不能成为QThread对象的子对象,因为QThread对象不在调用QThread::run()的线程中。注意:QObject的成员变量不会自动成为其子变量。必须通过向子构造函数传递指针或调用setParent()来设置父子关系。如果没有此步骤,调用moveToThread()时,对象的成员变量将保留在旧线程中。
无复制构造函数和赋值运算符
QObject既没有复制构造函数,也没有赋值运算符。它们是在宏Q_DISABLE_COPY()中设置了禁止生成。实际上,所有从QObject派生的Qt类(直接或间接)都使用这个宏来声明它们的复制构造函数和赋值操作符是私有的。
这带来的结果是应该使用指向QObject(或者指向您的QObject子类)的指针。例如,如果没有复制构造函数,就不能使用QObject的子类作为存储在某个容器类中的值,必须存储指针。
QThreadPool + QRunnable
QRunnable类同QThread类似,需要重写run函数,且必须要借助于线程池启动线程,但由于不是继承QObject,无法使用信号槽与外界通信,如果要使用QRunnablel与外界通信,则需要:
1. 使用多重继承(有一个必须继承QObject);
2. 使用QMetaObject::invokeMethod(需要维护和外界通信的QObject对象成员)。
m_pRunnable = new CusRunnable(this);
QThreadPool threadpool;
threadpool.setMaxThreadCount(1);//设置本地线程池最大为1.
threadpool.start(m_pRunnable);
//QThreadPool::globalInstance()->start(m_pRunnable);// 或使用全局线程池。
// 重新run,并与m_pObj对象通信
void CusRunnable::run()
{
qDebug() << __FUNCTION__ <<"other Thread:"<< QThread::currentThreadId();
//实现与主线程通信, m_pObj对象有一个:Q_INVOKABLE void setText(QString msg);
QMetaObject::invokeMethod(m_pObj,"setText",Q_ARG(QString,"this is AA!"));
QThread::msleep(1000);
}
既然QThread这么简单我们为什么还要使用QThreadPool + QRunnable创建线程池的方法来使用线程机制呢?
主要原因:当线程任务量非常大的时候,每个任务都用一个QThread来执行,会占用全局线程池量,同时会频繁创建和释放QThread对象,带来比较大的内存开销,而线程池则可以有效避免该问题。创建的指定的线程个数,直到任务完成后才释放(线程id一直不变)
#include <QObject>
#include <QRunnable>
#include <QThread>
#include <QThreadPool>
#include <QDebug>
class HelloWorldTask : public QRunnable
{
// 线程执行任务:每间隔1s打印出线程的信息
void run()
{
int m_dataMem[256*1000]; // 占约 1MB内存空间
for (int nCount = 0; nCount < 5; nCount++)
{
qDebug() << QThread::currentThread();
QThread::msleep(1000);
}
}
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QThreadPool threadpool;
threadpool.setMaxThreadCount(3);
for (int nNum = 0; nNum < 100; nNum++)
{
threadpool.start(new HelloWorldTask); //可被多个不同任务替换。,但都交给线程池完成。
QThread::msleep(1000);
}
return a.exec();
}
通过线程池,实现多个任务由三个线程完成(只有所有任务结束之后,三个线程才终止并释放资源。)
QtConcurrent
QtConcurrent 是一个命名空间,主要为多线程服务,它提供了高层次的函数接口 (APIs),使所写程序,可根据计算机的 CPU 核数,自动调整运行的线程数目。会在一个单独的线程中执行,并且该线程取自全局QThreadPool,该函数的返回值通过QFuture API提供。
Qt Concurrent已经从 QtCore 中移除并成为了一个独立的模块。使用时,需要添加:
Qt += concurrent
几种用法:
QtConcurrent::run(Function function, …) 等价于 QtConcurrent::run(QThreadPool::globalInstance(), function, …);
QtConcurrent :: run()也接受指向成员函数的指针。第一个参数必须是一个const引用或一个指向该类实例的指针
使用Lambda函数
QFuture<void> future = QtConcurrent::run([](=) {
// Code in this block will run in another thread }
);
使用成员函数
extern void myFunc(QString &str);
QFuture<void> f2 =QtConcurrent::run(this,&Widget::myFunc,QString("aaa"));
调用外部函数
QFuture<void> f1 =QtConcurrent::run(func,QString(index++));
f1.waitForFinished();
相对于movetoThread(),他的好处就相当于是创建一个线程,用来跑指定对象的某个任务(或者全局的某个任务)。
线程执行后,需要进行wait()进行阻塞,否则,线程退出会报错:QThread: Destroyed while thread is still running
与线程相关的connect连接类型
Qt::AutoConnection
默认连接类型,如果信号接收方与发送方在同一个线程,则使用 Qt::DirectConnection,否则使用 Qt::QueuedConnection;连接类型在信号 发射时决定。
Qt::DirectConnection
信号所连接的槽函数将会被立即执行,并且是在发射信号的线程;倘若槽函数执行的是耗时操作、信号由UI线程发射,则会阻塞Qt的事件循环,UI会进入无响应状态 。
Qt::QueuedConnection
槽函数将会在接收者的线程被执行,此种连接类型下的信号倘若被多次触发、相应的槽函数会在接收者的线程里被顺次执行相应次数;当使用 QueuedConnection 时,参数类型必须是Qt基本类型,或者使用 qRegisterMetaType() 进行注册了的自定义类型。
如使用信号:void sigOpenBuyPage(FFBuyPageType type);需要在构造函数时,进行注册: qRegisterMetaType(“FFBuyPageType”);
Qt::BlockingQueuedConnection
和 Qt::QueuedConnection 类似,区别在于发送信号的线程在槽函数执行完毕之前一直处于阻塞状态;收发双方必须不在同一线程,否则会导致死锁 。
如,当信号在本类实现,并阻塞式连接,将发生死锁。故信号槽必须禁止阻塞连接。
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = 0);
~Widget();
public slots:
void slotHandTimeOut();
private:
Ui::Widget *ui;
QTimer m_task_timer;
};
//cpp
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
m_task_timer.start(1000);
connect(&m_task_timer,&QTimer::timeout,this,&Widget::slotHandTimeOut,Qt::BlockingQueuedConnection);
}
Widget::~Widget()
{
delete ui;
}
void Widget::slotHandTimeOut()
{
qDebug()<<"timeout currentThreadId:"<<QThread::currentThreadId();
qDebug()<<__FILE__<<"parent"<<parent();
}
Qt::UniqueConnection
执行方式与 AutoConnection 相同,不过关联是唯一的。(如果相同两个对象,相同的信号关联到相同的槽,那么第二次 connect 将失败)
互斥锁:QMutex
QMutex
,任意时刻至多有一个线程可以使用该锁,若一个线程尝试获取 mutex
,而此时 mutex
已被锁住。则这儿线程将休眠直到 mutex解锁
为止。互斥锁经常用于共享数据。
tryLock();//非阻塞
lock();//阻塞
unlock();
例:
QMutex mute;
mute.lock()
mute.unlock();
QMutexLocker这个类是基于QMutex的便利类,这个类不能够定义私有成员变量和全局变量,只能够定义局部变量来使用。
如果需要对一个全局变量区域进行保护,那么QMutex定义的变量就得是全局的,如果是对某个类作用域进行保护,则QMutex可以定义成员变量。
如:
void 类名::deleteList()
{
QMutexLocker locker(&m_Mtx);
for(auto media:m_list)
{
delete media;
}
}
QMutex mutex;
void thread1()
{
mutex.lock();
//dosomething()
mutex.unlock();
}
void thread2()
{
mutex.lock();
//dosomething()
mutex.unlock();
}
QMutexLocker上锁,解锁的原理:
在该局部变量被创建的时候上锁,当所在函数运行完毕后该QMutexLocker局部变量在栈中销毁掉,根据他自己的机制也就相对应的解锁了。注意,如果该局部变量在中间被打断,那么QMutexLocker上的锁就不会被解锁掉,因为该函数没有被完整的是执行完。QMutexLocker所创建的局部变量也没有被正确销毁销毁,可能就和QMutexLocker他自己本身的机制不服也就不会解锁。
读写锁QReadWriteLock
QReadWriteLock
,与 QMutex
类似,不过它允许多个线程对共享数据进行读取,写独占。使用它替代 QMutex
可提高多线程程序的并发度。
void lockForRead()
void lockForWrite()
void unlock()
读写锁的使用场景:在一些共享资源的读和写操作,且写操作没有读操作那么频繁的场景下可以用读写锁。
QReadWriteLock lock;
void ReaderThread::run()
{
...
lock.lockForRead();
read_file();
lock.unlock();
...
}
void WriterThread::run()
{
...
lock.lockForWrite();
write_file();
lock.unlock();
...
}
QReadLocker/QWriteLocker
与QMutexLocker相似,在构造时lock(),在析构时unlock();
信号量QSemaphore
QSemaphore
,QMutex
的一般化,用于保护一定数量的相同的资源。典型的是 生成者-消费者。
void acquire(int n = 1)
int available() const
void release(int n = 1)//可用于新建信号量资源
bool tryAcquire(int n = 1)
bool tryAcquire(int n, int timeout)
QSemaphore sem(5); // sem.available() == 5
sem.acquire(3); // sem.available() == 2
sem.acquire(2); // sem.available() == 0
sem.release(5); // sem.available() == 5
sem.release(5); // sem.available() == 10
sem.tryAcquire(1); // sem.available() == 9, returns true
sem.tryAcquire(250); // sem.available() == 9, returns false
条件变量 QWaitCondition
WaitCondition 用于多线程的同步,一个线程调用QWaitCondition::wait()阻塞等待,直到另一个线程调用QWaitCondition::wake()唤醒才继续往下执行。
wait() 函数必须传入一个已上锁的 mutex 对象,等待过程中,mutex会解锁
示例:生成者与消费者
//不严谨用法:
// 示例一
// 主线程
Send(&packet);
mutex.lock();
condition.wait(&mutex);
if (m_receivedPacket)
{
HandlePacket(m_receivedPacket); // 另一线程传来回包
}
mutex.unlock();
// 通信线程
m_receivedPacket = ParsePacket(buffer); // 将接收的数据解析成包
mutex.lock();
condition.wakeAll();
mutex.unlock();
主线程中,调用 Send(&packet) 发送后,假如通信线程立即收到回包,在主线程还来不及调用 wait() 的时候,已经先 wakeAll() 了,显然这次唤醒是无效的,但主线程继续调用wait(),然后一直阻塞在那里。
//通过条件变量,实现
//严谨用法
// 示例二
// 主线程
mutex.lock();
Send(&packet);//作为原子操作
condition.wait(&mutex);
if (m_receivedPacket)
{
HandlePacket(m_receivedPacket); // 另一线程传来回包
}
mutex.unlock();
// 通信线程
m_receivedPacket = ParsePacket(buffer); // 将接收的数据解析成包
mutex.lock();
condition.wakeAll();
mutex.unlock();
//源码
//qthread.cpp
QThread::QThread(QObject *parent): QObject(*(new QThreadPrivate), parent);
QThread::QThread(QThreadPrivate &dd, QObject *parent): QObject(dd, parent);
QThread::~QThread();
bool QThread::isFinished() const;
bool QThread::isRunning() const;
void QThread::setStackSize(uint stackSize);
int QThread::exec();
void QThread::exit(int returnCode);
void QThread::quit();
void QThread::run();
void QThread::setPriority(Priority priority);