QT开发(三十四)——QT多线程编程
一、QT多线程简介
QT通过三种形式提供了对线程的支持,分别是平台无关的线程类、线程安全的事件投递、跨线程的信号-槽连接。
QT中线程类包含如下:
QThread 提供了开始一个新线程的方法
QThreadStorage 提供逐线程数据存储
QMutex 提供相互排斥的锁,或互斥量
QMutexLocker 是一个辅助类,自动对 QMutex 加锁与解锁
QReadWriterLock 提供了一个可以同时读操作的锁
QReadLocker与QWriteLocker 自动对QReadWriteLock 加锁与解锁
QSemaphore 提供了一个整型信号量,是互斥量的泛化
QWaitCondition 提供了一种方法,使得线程可以在被另外线程唤醒之前一直休眠。
QThread的两种使用方法:
A、不使用事件循环。
a. 子类化 QThread
b. 重载 run 函数,run函数内有一个 while 或 for 的死循环
c. 设置一个标记为来控制死循环的退出。
B、使用事件循环。
a. 子类化 QThread,
b. 重载 run 使其调用 QThread::exec()
c. 为子类定义信号和槽,由于槽函数并不会在新开的 thread 运行,很多人为了解决这个问题在构造函数中调用 moveToThread(this)
Bradley T. Hughes 给出说明是: QThread 应该被看做是操作系统线程的接口或控制点,而不应该包含需要在新线程中运行的代码。需要运行的代码应该放到一个QObject的子类中,然后将该子类的对象moveToThread到新线程中。
在Qt4.4之前,run 是虚函数,必须子类化QThread来实现run函数。
而从Qt4.4开始,QThread不再支持抽象类,run 默认调用 QThread::exec() ,不需要子类化 QThread 了,只需要子类化一个 QObject 。
二、QThread线程
QThread是Qt线程中有一个公共的抽象类,所有的线程都是从QThread抽象类中派生的,要实现QThread中的纯虚函数run(),run()函数是通过start()函数来实现调用的。
创建线程对象的实例,调用QThread::start(),在子线程类run()里出现的代码将会在新建线程中被执行。
QCoreApplication::exec()总是在主线程(执行main()的那个线程)中被调用,不能从一个QThread中调用。在GUI程序中,主线程也被称为GUI线程,是唯一允许执行GUI相关操作的线程。另外,必须在创建一个QThread之前创建QApplication(or QCoreApplication)对象。
1、线程的优先级
QThread线程总共有8个优先级
QThread::IdlePriority0scheduled only when no other threads are running.
QThread::LowestPriority1scheduled less often than LowPriority.
QThread::LowPriority2scheduled less often than NormalPriority.
QThread::NormalPriority3the default priority of the operating system.
QThread::HighPriority4scheduled more often than NormalPriority.
QThread::HighestPriority5scheduled more often than HighPriority.
QThread::TimeCriticalPriority6scheduled as often as possible.
QThread::InheritPriority7use the same priority as the creating thread. This is the default.
2、线程的创建
void start ( Priority priority = InheritPriority )
启动线程执行,启动后会发出started ()信号
3、线程的执行
int exec();
进入线程时间循环
virtual void run();
线程入口
4、线程的退出
void quit();
相当于exit(0);
void exit ( int returnCode = 0 );
调用exit后,thread将退出event loop,并从exec返回,exec的返回值就是returnCode。通常returnCode=0表示成功,其他值表示失败。
void terminate ();
结束线程,线程是否立即终止取决于操作系统。
线程被终止时,所有等待该线程Finished的线程都将被唤醒。
terminate是否调用取决于setTerminationEnabled ( bool enabled = true )开关。
5、线程的等待
void msleep ( unsigned long msecs )
void sleep ( unsigned long secs )
void usleep ( unsigned long usecs )
bool wait ( unsigned long time = ULONG_MAX )
线程将会被阻塞,等待time毫秒,如果线程退出,则wait会返回。
6、线程的状态
bool isFinished () const线程是否已经退出
bool isRunning () const线程是否处于运行状态
7、线程的属性
Priority priority () const
void setPriority ( Priority priority )
uint stackSize () const
void setStackSize ( uint stackSize )
void setTerminationEnabled ( bool enabled = true )
设置是否响应terminate()函数
8、线程与事件循环
QThread中对run()的默认实现调用了exec(),从而创建一个QEventLoop对象,由QEventLoop对象处理线程中事件队列(每一个线程都有一个属于自己的事件队列)中的事件。exec()在其内部不断做着循环遍历事件队列的工作,调用QThread的quit()或exit()方法使退出线程,尽量不要使用terminate()退出线程,terminate()退出线程过于粗暴,造成资源不能释放,甚至互斥锁还处于加锁状态。
线程中的事件循环,使得线程可以使用那些需要事件循环的非GUI 类(如,QTimer,QTcpSocket,QProcess)。
在QApplication之前创建的对象,QObject::thread()返回0,这意味着主线程仅为这些对象处理投递事件,不会为没有所属线程的对象处理另外的事件。可以用QObject::moveToThread()来改变它和它孩子们的线程亲缘关系,假如对象有父亲,它不能移动这种关系。在另一个线程(而不是创建它的那个线程)中delete QObject对象是不安全的。除非你可以保证在同一时刻对象不在处理事件。可以用QObject::deleteLater(),它会投递一个DeferredDelete事件,这会被对象线程的事件循环最终选取到。假如没有事件循环运行,事件不会分发给对象。举例来说,假如你在一个线程中创建了一个QTimer对象,但从没有调用过exec(),那么QTimer就不会发射它的timeout()信号.对deleteLater()也不会工作。(这同样适用于主线程)。你可以手工使用线程安全的函数QCoreApplication::postEvent(),在任何时候,给任何线程中的任何对象投递一个事件,事件会在那个创建了对象的线程中通过事件循环派发。事件过滤器在所有线程中也被支持,不过它限定被监视对象与监视对象生存在同一线程中。类似地,QCoreApplication::sendEvent(不是postEvent()),仅用于在调用此函数的线程中向目标对象投递事件。
三、线程的同步
QMutex, QReadWriteLock, QSemaphore, QWaitCondition 提供了线程同步的手段。使用线程的主要想法是希望它们可以尽可能并发执行,而一些关键点上线程之间需要停止或等待。例如,假如两个线程试图同时访问同一个 全局变量,结果可能不如所愿。
1、互斥量QMutex
QMutex 提供相互排斥的锁,或互斥量。在一个时刻至多一个线程拥有mutex,假如一个线程试图访问已经被锁定的mutex,那么它将休眠,直到拥有mutex的线程对此mutex解锁。QMutex常用来保护共享数据访问。QMutex类所以成员函数是线程安全的。
头文件声明: #include <QMutex>
互斥量声明: QMutex m_Mutex;
互斥量加锁: m_Mutex.lock();
互斥量解锁: m_Mutex.unlock();
QMutex ( RecursionMode mode = NonRecursive )
QMutex有两种模式:Recursive, NonRecursive
A、Recursive
一个线程可以对mutex多次lock,直到相应次数的unlock调用后,mutex才真正被解锁。
B、NonRecursive
默认模式,mutex只能被lock一次。
如果使用了Mutex.lock()而没有对应的使用Mutex.unlcok()的话就会造成死锁,其他的线程将永远也得不到接触Mutex锁住的共享资源的机会。尽管可以不使用lock()而使用tryLock(timeout)来避免因为死等而造成的死锁( tryLock(负值)==lock()),但是还是很有可能造成错误。
bool tryLock();
如果当前其他线程已对该mutex加锁,则该调用会立即返回,而不被阻塞。
bool tryLock(int timeout);
如果当前其他线程已对该mutex加锁,则该调用会等待一段时间,直到超时
QMutex mutex; int complexFunction(int flag) { mutex.lock(); int retVal = 0; switch (flag) { case 0: case 1: mutex.unlock(); return moreComplexFunction(flag); case 2: { int status = anotherFunction(); if (status < 0) { mutex.unlock(); return -2; } retVal = status + flag; } break; default: if (flag > 10) { mutex.unlock(); return -1; } break; } mutex.unlock(); return retVal; }
2、互斥锁QMutexLocker
在较复杂的函数和异常处理中对QMutex类mutex对象进行lock()和unlock()操作将会很复杂,进入点要lock(),在所有跳出点都要unlock(),很容易出现在某些跳出点未调用unlock(),所以Qt引进了QMutex的辅助类QMutexLocker来避免lock()和unlock()操作。在函数需要的地方建立QMutexLocker对象,并把mutex指针传给QMutexLocker对象,此时mutex已经加锁,等到退出函数后,QMutexLocker对象局部变量会自己销毁,此时mutex解锁。
头文件声明: #include<QMutexLocker>
互斥锁声明: QMutexLocker mutexLocker(&m_Mutex);
互斥锁加锁: 从声明处开始(在构造函数中加锁)
互斥锁解锁: 出了作用域自动解锁(在析构函数中解锁)
QMutex mutex; int complexFunction(int flag) { QMutexLocker locker(&mutex); int retVal = 0; switch (flag) { case 0: case 1: return moreComplexFunction(flag); case 2: { int status = anotherFunction(); if (status < 0) return -2; retVal = status + flag; } break; default: if (flag > 10) return -1; break; } return retVal; }
3、QReadWriteLock
QReadWriterLock 与QMutex相似,但对读写操作访问进行区别对待,可以允许多个读者同时读数据,但只能有一个写,并且写读操作不同同时进行。使用QReadWriteLock而不是QMutex,可以使得多线程程序更具有并发性。QReadWriterLock默认模式是NonRecursive。
QReadWriterLock类成员函数如下:
QReadWriteLock ( )
QReadWriteLock ( RecursionMode recursionMode )
void lockForRead ()
void lockForWrite ()
bool tryLockForRead ()
bool tryLockForRead ( int timeout )
bool tryLockForWrite ()
bool tryLockForWrite ( int timeout )
boid unlock ()
使用实例:
QReadWriteLock lock; void ReaderThread::run() { lock.lockForRead(); read_file(); lock.unlock(); } void WriterThread::run() { lock.lockForWrite(); write_file(); lock.unlock(); }
4、QReadLocker和QWriteLocker
在较复杂的函数和异常处理中对QReadWriterLock类lock对象进行lockForRead()/lockForWrite()和unlock()操作将会很复杂,进入点要lockForRead()/lockForWrite(),在所有跳出点都要unlock(),很容易出现在某些跳出点未调用unlock(),所以Qt引进了QReadLocker和QWriteLocker类来简化解锁操作。在函数需要的地方建立QReadLocker或QWriteLocker对象,并把lock指针传给QReadLocker或QWriteLocker对象,此时lock已经加锁,等到退出函数后,QReadLocker或QWriteLocker对象局部变量会自己销毁,此时lock解锁。
QReadWriteLock lock;
QByteArray readData()
{
lock.lockForRead();
...
lock.unlock();
return data;
}
使用QReadLocker:
QReadWriteLock lock;
QByteArray readData()
{
QReadLocker locker(&lock);
...
return data;
}
5、信号量QSemaphore
QSemaphore 是QMutex的一般化,可以保护一定数量的相同资源,而一个mutex只保护一个资源。QSemaphore 类的所有成员函数是线程安全的。
经典的生产者-消费者模型如下:某工厂只有固定仓位,生产人员每天生产的产品数量不一,销售人员每天销售的产品数量也不一致。当生产人员生产P个产品时,就一次需要P个仓位,当销售人员销售C个产品时,就要求仓库中有足够多的产品才能销售。如果剩余仓位没有P个时,该批次的产品都不存入,当当前已有的产品没有C个时,就不能销售C个以上的产品,直到新产品加入后方可销售。
QSemaphore来控制对环状缓冲的访问,此缓冲区被生产者线程和消费者线程共享。生产者不断向缓冲区写入数据直到缓冲末端,再从头开始。消费者从缓冲不断读取数据。信号量比互斥量有更好的并发性,假如我们用互斥量来控制对缓冲的访问,那么生产者、消费者不能同时访问缓冲区。然而,我们知道在同一时刻,不同线程访问缓冲的不同部分并没有什么危害。
QSemaphore 类成员函数:
QSemaphore ( int n = 0 )
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
生产者-消费者实例:
#include <QtCore/QCoreApplication> #include <QSemaphore> #include <QThread> #include <cstdlib> #include <cstdio> const int DataSize = 100000; const int BufferSize = 8192; char buffer[BufferSize]; QSemaphore production(BufferSize); QSemaphore consumption; class Producor:public QThread { public: void run(); }; void Producor::run() { for(int i = 0; i < DataSize; i++) { production.acquire(); buffer[i%BufferSize] = "ACGT"[(int)qrand()%4]; consumption.release(); } } class Consumer:public QThread { public: void run(); }; void Consumer::run() { for(int i = 0; i < DataSize; i++) { consumption.acquire(); fprintf(stderr, "%c", buffer[i%BufferSize]); production.release(); } fprintf(stderr, "%c", "\n"); } int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); Producor productor; Consumer consumer; productor.start(); consumer.start(); productor.wait(); consumer.wait(); return a.exec(); }
Producer::run函数:
当producer线程执行run函数,如果buffer中已满,而consumer线程没有读,producer不能再往buffer中写字符,在 productor.acquire 处阻塞直到 consumer线程读(consume)数据。一旦producer获取到一个字节(资源)就写入一个随机的字符,并调用 consumer.release 使consumer线程可以获取一个资源(读一个字节的数据)。
Consumer::run函数:
当consumer线程执行run函数,如果buffer中没有数据,则consumer线程在consumer.acquire处阻塞,直到producer线程执行写操作写入一个字节,并执行consumer.release 使consumer线程的可用资源数=1时,consumer线程从阻塞状态中退出, 并将consumer 资源数-1,consumer当前资源数=0。
6、等待条件QWaitCondition
QWaitCondition 允许线程在某些情况发生时唤醒另外的线程。一个或多个线程可以阻塞等待QWaitCondition ,用wakeOne()或wakeAll()设置一个条件。wakeOne()随机唤醒一个,wakeAll()唤醒所有。
QWaitCondition ()
bool wait ( QMutex * mutex, unsigned long time = ULONG_MAX )
bool wait ( QReadWriteLock * readWriteLock, unsigned long time = ULONG_MAX )
void wakeOne ()
void wakeAll ()
头文件声明: #include <QWaitCondition>
等待条件声明: QWaitCondtion m_WaitCondition;
等待条件等待: m_WaitConditon.wait(&m_muxtex, time);
等待条件唤醒: m_WaitCondition.wakeAll();
在经典的生产者-消费者场合中,生产者首先必须检查缓冲是否已满(numUsedBytes==BufferSize),如果缓冲区已满,线程停下来等待 bufferNotFull条件。如果没有满,在缓冲中生产数据,增加numUsedBytes,激活条件 bufferNotEmpty。使用mutex来保护对numUsedBytes的访问。QWaitCondition::wait() 接收一个mutex作为参数,mutex被调用线程初始化为锁定状态。在线程进入休眠状态之前,mutex会被解锁。而当线程被唤醒时,mutex会处于锁定状态,从锁定状态到等待状态的转换是原子操作。当程序开始运行时,只有生产者可以工作,消费者被阻塞等待bufferNotEmpty条件,一旦生产者在缓冲中放入一个字节,bufferNotEmpty条件被激发,消费者线程于是被唤醒。
#include <QtCore/QCoreApplication> #include <QSemaphore> #include <QThread> #include <cstdlib> #include <cstdio> #include <QWaitCondition> #include <QMutex> #include <QTime> const int DataSize = 32; const int BufferSize = 16; char buffer[BufferSize]; QWaitCondition bufferNotEmpty; QWaitCondition bufferNotFull; QMutex mutex; int used = 0; class Producor:public QThread { public: void run(); }; void Producor::run() { qsrand(QTime(0,0,0).secsTo(QTime::currentTime())); for(int i = 0; i < DataSize; i++) { mutex.lock(); if(used == BufferSize) bufferNotFull.wait(&mutex); mutex.unlock(); buffer[i%BufferSize] = used; mutex.lock(); used++; bufferNotEmpty.wakeAll(); mutex.unlock(); } } class Consumer:public QThread { public: void run(); }; void Consumer::run() { for(int i = 0; i < DataSize; i++) { mutex.lock(); if(used == 0) bufferNotEmpty.wait(&mutex); mutex.unlock(); fprintf(stderr, "%d\n", buffer[i%BufferSize]); mutex.lock(); used--; bufferNotFull.wakeAll(); mutex.unlock(); } fprintf(stderr, "%c", "\n"); } int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); Producor productor; Consumer consumer; productor.start(); consumer.start(); productor.wait(); consumer.wait(); return a.exec(); }
四、可重入与线程安全
可重入reentrant与线程安全thread-safe被用来说明一个函数如何用于多线程程序。假如一个类的任何函数在此类的多个不同的实例上,可以被多个线程同时调用,那么这个类被称为是可重入的。假如不同的线程作用在同一个实例上仍可以正常工作,那么称之为“线程安全”的。
1、可重入
大多数c++类天生就是可重入的,因为它们典型地仅仅引用成员数据。任何线程可以在类的一个实例上调用这样的成员函数,只要没有别的线程在同一个实例上调用这个成员函数。
class Counter
{
public:
Counter() {n=0;}
void increment() {++n;}
void decrement() {--n;}
int value() const {return n;}
private:
int n;
};
Counter类是可重入的,但却不是线程安全的。假如多个线程都试图修改数据成员n,结果未定义。
大多数Qt类是可重入,非线程安全的。有一些类与函数是线程安全的,它们主要是线程相关的类,如QMutex,QCoreApplication::postEvent()。
2、线程安全
所有的GUI类(比如,QWidget和它的子类),操作系统核心类(比如,QProcess)和网络类都不是线程安全的。
class Counter
{
public:
Counter() { n = 0; }
void increment() { QMutexLocker locker(&mutex); ++n; }
void decrement() { QMutexLocker locker(&mutex); --n; }
int value() const { QMutexLocker locker(&mutex); return n; }
private:
mutable QMutex mutex;
int n;
};
Counter类是可重入和线程安全的。QMutexLocker类在构造函数中自动对mutex进行加锁,在析构函数中进行解锁。mutex使用了mutable关键字来修饰,因为在value()函数中对mutex进行加锁与解锁操作,而value()是一个const函数。
QObject与线程
QThread 继承自QObject,它发射信号以指示线程执行开始与结束,而且也提供了许多slots。更有趣的是,QObjects可以用于多线程,这是因为每个线程被允许有它自己的事件循环。
QObject 可重入性
QObject是可重入的。它的大多数非GUI子类,像 QTimer,QTcpSocket,QUdpSocket,
QHttp,QFtp,QProcess也是可重入的,在多个线程中同时使用这些类是可能 的。需要注意的是,这些类被设计成在一个单线程中创建与使用,因此,在一个线程中创建一个对象,而在另外的线程中调用它的函数,这样的行为不能保证工作良好。有三种约束需要注意:
1,QObject的孩子总是应该在它父亲被创建的那个线程中创建。这意味着,你绝不应该传递QThread对象作为另一个对象的父亲(因为QThread对象本身会在另一个线程中被创建)
2,事件驱动对象仅仅在单线程中使用。明确地说,这个规则适用于"定时器机制“与”网格模块“,举例来讲,你不应该在一个线程中开始一个定时器或是连接一个套接字,当这个线程不是这些对象所在的线程。
3,你必须保证在线程中创建的所有对象在你删除QThread前被删除。这很容易做到:你可以run()函数运行的栈上创建对象。
尽管QObject是可重入的,但GUI类,特别是QWidget与它的所有子类都是不可重入的。 它们仅用于主线程。正如前面提到过的,QCoreApplication::exec()也必须从那个线程中被调用。实践上,不会在别的线程中使用GUI 类,它们工作在主线程上,把一些耗时的操作放入独立的工作线程中,当工作线程运行完成,把结果在主线程所拥有的屏幕上显示。
五、线程与信号和槽
run 是线程的入口,run的开始和结束意味着线程的开始和结束,run函数中的代码在新建线程中执行。
可以把任何线程的signals连接到特定线程的slots,也就是说信号-槽机制是可以跨线程使用的。
bool QObject::connect ( const QObject * sender, const char * signal, const QObject * receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection )
Qt::AutoConnection类型:Qt支持6种连接方式
A、Qt::DirectConnection(直连方式)(信号与槽函数关系类似于函数调用,同步执行)
当信号发出后,相应的槽函数将立即被调用。emit语句后的代码将在所有槽函数执行完毕后被执行。
当信号发射时,槽函数将直接被调用。
无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行。
B、Qt::QueuedConnection(队列方式)(此时信号被塞到信号队列里了,信号与槽函数关系类似于消息通信,异步执行)
当信号发出后,排队到信号队列中,需等到接收对象所属线程的事件循环取得控制权时才取得该信号,调用相应的槽函数。emit语句后的代码将在发出信号后立即被执行,无需等待槽函数执行完毕。
当控制权回到接受者所依附线程的事件循环时,槽函数被调用。
槽函数在接收者所依附线程执行。
C、Qt::AutoConnection(自动方式)
Qt的默认连接方式,如果信号的发出和接收这个信号的对象同属一个线程,那个工作方式与直连方式相同;否则工作方式与排队方式相同。
如果信号在接收者所依附的线程内发射,则等同于直接连接
如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接
D、Qt::BlockingQueuedConnection(信号和槽必须在不同的线程中,否则就产生死锁)
这个是完全同步队列只有槽线程执行完成才会返回,否则发送线程也会一直等待,相当于是不同的线程可以同步起来执行。
E、Qt::UniqueConnection
与默认工作方式相同,只是不能重复连接相同的信号和槽,因为如果重复连接就会导致一个信号发出,对应槽函数就会执行多次。
F、Qt::AutoCompatConnection
是为了连接Qt4与Qt3的信号槽机制兼容方式,工作方式与Qt::AutoConnection一样。
如果这个参数不设置的话,默认表示的是那种方式呢?
没加的话与直连方式相同:当信号发出后,相应的槽函数将立即被调用。emit语句后的代码将在所有槽函数执行完毕后被执行。在这个线程内是顺序执行、同步的,但是与其它线程之间肯定是异步的了。如果使用多线程,仍然需要手动同步。
slot 函数属于我们在main中创建的对象 thread,即thread依附于主线程
队列连接告诉我们:槽函数在接受者所依附线程执行。即 slot 将在主线程执行
直接连接告诉我们:槽函数在发送信号的线程执行。
自动连接告诉我们:二者不同,等同于队列连接。即 slot 在主线程执行
QThread是用来管理线程的,QThread对象所依附的线程和所管理的线程并不是同一个概念。QThread所依附的线程,就是创建QThread对象的线程,QThread 所管理的线程,就是run启动的线程,也就是新建线程。QThread对象依附在主线程中,QThread对象的slot函数会在主线程中执行,而不是次线程。除非:
QThread对象依附到次线程中(通过movetoThread)
slot 和信号是直接连接,且信号在次线程中发射