Qt 应用程序中所有的界面响应事件都在一个主线程中运行,当我们去调用QApplication对象的exec()方法时,Qt就不断去循环查询当前的事件队列中有没有事件发生,如果有则转去执行对应的槽函数,如果将过多的业务逻辑及耗时的操作放到当前的主线程中去执行,则界面的响应事件就无法及时得到响应,整个界面就会卡顿。
一般Qt应用程序的开发分为界面和后台两部分,在主线程中执行界面显示相关的操作,其他的业务逻辑则放到线程中去作为后台程序去执行,线程和线程及线程和主线程之间通过信号和槽进行通讯。
多线程开发中,常用的组件有线程、消息队列、信号量、互斥锁。一般一个线程维护一个消息队列,线程不断的查询或阻塞等待直到队列中有消息,则去执行相应的操作,并使用信号量进行线程间的同步,使用互斥锁保护线程间的共享资源。在Qt中也有线程、信号量、互斥锁组件,但消息队列则换成了信号&&槽机制,Qt中的每个线程自身单独维护一个事件队列,我们将不同的线程对象的信号和槽关联在一起,当事件发生时,对应信号则从一个线程发送到另一个线程的事件队列中,当前线程获取信号后,执行对应的槽函数,执行完成后再次进入事件监测循环中。
Qt实现多线程有两种方式,一种是基于QThread 类,使用时定义一个新的类继承QThread类,并重写QThread的run()方法,需要注意的是,这种方式只有run()方法是在新线程中执行的,其他的方法还是在创建对象的线程中执行。这种方法官方不推荐使用,已经被淘汰了,我们仅了解一下就可以。第二种方法是基于QObject类,使用时定义一个新的类继承QObject类,调用新类对象的moveToThread()方法绑定当前对象到一个线程中。该种方式新类的所有方法都被放在新的线程中执行。
1. QThread对象的创建
定义一个新类 继承 QObject类(新类的实例化不能设置任何父对象)。
实例化一个QThread对象。
实现新类中的槽函数。
QObject子类对象通过moveToThread将自己放到线程QThread对象中。
调用QThread对象的start函数启动线程。
关联不同对象的信号和槽到新的类实例上。
2. QThread对象的销毁
先把QObject在线程循环中释放(使用QObject::deleteLater函数),然后QThread::quit,然后QThread::wait。
1. 把线程的finished信号和object的deleteLater槽进行关联,线程退出时,释放QObject对象申请的内存空间。
2. 在析构函数中调用QThread::quit方法退出线程的事件循环。
3. 在析构函数中调用QThread::wait 回收线程资源。
我们看一下官方代码:
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork(const QString ¶meter) {
QString result;
emit resultReady(result);
}
signals:
void resultReady(const QString &result);
};
class Controller : public QObject
{
Q_OBJECT
QThread workerThread; //实例化线程对象
public:
Controller() {
Worker *worker = new Worker; //实例化新类(不能指定父对象)
worker->moveToThread(&workerThread);
connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater); //释放QObject资源
connect(this, &Controller::operate, worker, &Worker::doWork);
connect(worker, &Worker::resultReady, this, &Controller::handleResults);
workerThread.start();
}
~Controller() {
workerThread.quit(); //退出线程事件循环
workerThread.wait(); //回收线程资源
}
public slots:
void handleResults(const QString &);
signals:
void operate(const QString &);
};
3. 线程的同步-----信号量QSemphore
互斥锁保护的资源同一时刻只能有一个线程能够获取使用权,有些资源是可以限定多个线程同时访问,那么这个时候可以使用信号量。在Qt中信号量为QSemaphore。
QSemaphore sem(2) //实例化信号量对象,并设定初始值为2.
sem.acquire(); //获取信号量,获取不到阻塞等待
sem.tryAcquire(); //获取信号量,获取不到立即返回
semaphore.release(); //释放信号量
4. 线程共享资源的保护----互斥锁QMutex、QMutexLocker
QMutex类提供了一个保护一段临界区代码的方法,他每次只允许一个线程访问这段临界区代码。
QMutex mutex; //实例化互斥对象
mutex.lock(); //对共享资源加锁,获取不到,阻塞等待
mutex.tryLock(int timeout = 0) //获取不到锁,超时返回,time==0,立即返回。
mutex.unlock(); //释放锁
Qt提供了QMutexLocker类何以简化互斥量的处理,它在构造函数中接受一个QMutex对象作为参数并将其锁定,在析构函数中解锁这个互斥量。
QMutex mutex;
void func()
{
QMutexLocker locker(_mutex); //func()执行完后,自动释放锁。
}
5. 读写锁QReadWriteLocker、QReadLocker、QWriteLocker
不常用,暂不作说明。
6. QWaitCondition允许一个线程在一定条件下唤醒其他线程,生产这消费这模型
void Producer::run() //生产者
{
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (usedSpace == BufferSize)
bufferIsNotFull.wait(&mutex);
buffer[i % BufferSize] = "MING"[uint(rand()) % 4];
++usedSpace;
bufferIsNotEmpty.wakeAll();
mutex.unlock();
}
}
void Consumer::run() //消费者
{
forever {
mutex.lock();
if (usedSpace == 0)
bufferIsNotEmpty.wait(&mutex);
cerr << buffer[i % BufferSize];
--usedSpace;
bufferIsNotFull.wakeAll();
mutex.unlock();
}
cerr << endl;
}
bool wait ( QMutex * mutex, unsigned long time = ULONG_MAX );
这个函数做下说明,该函数将互斥量解锁并在此等待,它有两个参数,第一个参数为一个锁定的互斥量,第二个参数为等待时间。
调用wait()操作的线程使得作为参数的互斥量在调用前变为锁定状态,然后自身被阻塞变成为等待状态直到满足以下条件:
其他线程调用了wakeOne()或者wakeAll()函数,这种情况下将返回"true"值。
第二个参数time超时(以毫秒记时),该参数默认情况是ULONG_MAX,表示永不超时,这种情况下将返回"false"值。
wait()函数返回前会将互斥量参数重新设置为锁定状态,从而保证从锁定状态到等待状态的原则性转换。
7. QTimer定时器
头文件 #include
QTimer *timer = new QTimer(this); //this 指定父对象,可自动回收
connect(timer, SIGNAL(timeout()), this, SLOT(update()));
timer->start(1000);