纯净、安全、绿色的下载网站

首页|软件分类|下载排行|最新软件|IT学院

当前位置:首页IT学院IT技术

QT多线程编程 深入理解QT多线程编程

coolboywjun   2021-06-24 我要评论
想了解深入理解QT多线程编程的相关内容吗coolboywjun在本文为您仔细讲解QT多线程编程 的相关知识和一些Code实例欢迎阅读和指正我们先划重点:QT多线程编程,QT多线程下面大家一起来学习吧

一、线程基础

1、GUI线程与工作线程

每个程序启动后拥有的第一个线程称为主线程即GUI线程QT中所有的组件类和几个相关的类只能工作在GUI线程不能工作在次线程次线程即工作线程主要负责处理GUI线程卸下的工作

2、数据的同步访问

每个线程都有自己的栈因此每个线程都要自己的调用历史和本地变量线程共享相同的地址空间

二、QT多线程简介

QT通过三种形式提供了对线程的支持分别是平台无关的线程类、线程安全的事件投递、跨线程的信号-槽连接
QT中线程类包含如下:

  • QThread 提供了跨平台的多线程解决方案
  • QThreadStorage 提供逐线程数据存储
  • QMutex 提供相互排斥的锁或互斥量
  • QMutexLocker 是一个辅助类自动对 QMutex 加锁与解锁
  • QReadWriterLock 提供了一个可以同时读操作的锁
  • QReadLocker与QWriteLocker 自动对QReadWriteLock 加锁与解锁
  • QSemaphore 提供了一个整型信号量是互斥量的泛化
  • QWaitCondition 提供了一种方法使得线程可以在被另外线程唤醒之前一直休眠

三、QThread线程

1、QThread线程基础

    QThread是Qt线程中有一个公共的抽象类所有的线程类都是从QThread抽象类中派生的需要实现QThread中的虚函数run(),通过start()函数来调用run函数
    void run()函数是线程体函数用于定义线程的功能
    void start()函数是启动函数用于将线程入口地址设置为run函数
    void terminate()函数用于强制结束线程不保证数据完整性和资源释放
    QCoreApplication::exec()总是在主线程(执行main()的线程)中被调用不能从一个QThread中调用在GUI程序中主线程也称为GUI线程是唯一允许执行GUI相关操作的线程另外必须在创建一个QThread前创建QApplication(or QCoreApplication)对象
    当线程启动和结束时QThread会发送信号started()和finished()可以使用isFinished()和isRunning()来查询线程的状态
    从Qt4.8起可以释放运行刚刚结束的线程对象通过连接finished()信号到QObject::deleteLater()槽
    使用wait()来阻塞调用的线程直到其它线程执行完毕(或者直到指定的时间过去)
    静态函数currentThreadId()和currentThread()返回标识当前正在执行的线程前者返回线程的ID后者返回一个线程指针
    要设置线程的名称可以在启动线程之前调用setObjectName()如果不调用setObjectName()线程的名称将是线程对象的运行时类型(QThread子类的类名)

2、线程的优先级

    QThread线程总共有8个优先级

    QThread::IdlePriority   0 scheduled only when no other threads are running.
    QThread::LowestPriority  1 scheduled less often than LowPriority.
    QThread::LowPriority   2 scheduled less often than NormalPriority.
    QThread::NormalPriority  3 the default priority of the operating system.
    QThread::HighPriority   4 scheduled more often than NormalPriority.
    QThread::HighestPriority  5 scheduled more often than HighPriority.
    QThread::TimeCriticalPriority 6 scheduled as often as possible.
    QThread::InheritPriority   7 use the same priority as the creating thread. This is the default.
    void setPriority(Priority priority) 

    设置正在运行线程的优先级如果线程没有运行此函数不执行任何操作并立即返回使用的start()来启动一个线程具有特定的优先级优先级参数可以是QThread::Priority枚举除InheritPriortyd的任何值

3、线程的创建

 void start ( Priority priority = InheritPriority )

启动线程执行启动后会发出started ()信号

4、线程的执行

int exec() [protected] 

进入事件循环并等待直到调用exit()返回值是通过调用exit()来获得如果调用成功则返回0

void run() [virtual protected] 

线程的起点在调用start()之后新创建的线程就会调用run函数默认实现调用exec()大多数需要重新实现run函数便于管理自己的线程run函数返回时线程的执行将结束

5、线程的退出

void quit();

通知线程事件循环退出返回0表示成功相当于调用了QThread::exit(0)

void exit ( int returnCode = 0 );

调用exit后thread将退出event loop并从exec返回exec的返回值就是returnCode通常returnCode=0表示成功其他值表示失败

void terminate ();

结束线程线程是否立即终止取决于操作系统

线程被终止时所有等待该线程Finished的线程都将被唤醒

terminate是否调用取决于setTerminationEnabled ( bool enabled = true )开关

void requestInterruption() 

请求线程的中断请求是咨询意见并且取决于线程上运行的代码来决定是否及如何执行这样的请求此函数不停止线程上运行的任何事件循环并且在任何情况下都不会终止它

工程中线程退出的解决方案如下:

通过在线程类中增加标识变量volatile bool m_stop,通过m_stop变量的值判断run函数是否执行结束返回

#ifndef WORKTHREAD_H
#define WORKTHREAD_H
#include <QThread>
#include <QDebug>
class WorkThread : public QThread
{
protected:
  //线程退出的标识量
  volatile bool m_stop;
  void run()
  {
    qDebug() << "run begin";
    while(!m_stop)
    {
        //task handling
        int* p = new int[1000];
        for(int i = 0; i < 1000; i++)
        {
            p[i] = i * i;
        }
        sleep(2);
        delete [] p;
    }
    qDebug() << "run end";
  }
public:
  WorkThread()
    m_stop = false;
  //线程退出的接口函数用户使用
  void stop()
    m_stop = true;
};
#endif // WORKTHREAD_H

6、线程的等待

bool wait ( unsigned long time = ULONG_MAX )

线程将会被阻塞等待time毫秒,如果线程退出则wait会返回Wait函数解决多线程在执行时序上的依赖

void msleep ( unsigned long msecs )
void sleep ( unsigned long secs )
void usleep ( unsigned long usecs )

sleep()、msleep()、usleep()允许秒毫秒和微秒来区分但在Qt5.0中被设为public

一般情况下wait()和sleep()函数应该不需要因为Qt是一个事件驱动型框架考虑监听finished()信号来取代wait()使用QTimer来取代sleep()

7、线程的状态

bool isFinished () const  线程是否已经退出
bool isRunning () const   线程是否处于运行状态

8、线程的属性

Priority priority () const
void setPriority ( Priority priority )
uint stackSize () const
void setStackSize ( uint stackSize )
void setTerminationEnabled ( bool enabled = true )

设置是否响应terminate()函数

9、线程与事件循环

    QThread中run()的默认实现调用了exec()从而创建一个QEventLoop对象由QEventLoop对象处理线程中事件队列(每一个线程都有一个属于自己的事件队列)中的事件exec()在其内部不断做着循环遍历事件队列的工作调用QThread的quit()或exit()方法使退出线程尽量不要使用terminate()退出线程terminate()退出线程过于粗暴造成资源不能释放甚至互斥锁还处于加锁状态

    线程中的事件循环使得线程可以使用那些需要事件循环的非GUI 类(如QTimer,QTcpSocket,QProcess)

    在QApplication前创建的对象QObject::thread()返回NULL,意味着主线程仅为这些对象处理投递事件不会为没有所属线程的对象处理另外的事件可以用QObject::moveToThread()来改变对象及其子对象的线程亲缘关系假如对象有父亲不能移动这种关系在另一个线程(而不是创建它的线程)中delete QObject对象是不安全的除非可以保证在同一时刻对象不在处理事件可以用QObject::deleteLater(),它会投递一个DeferredDelete事件这会被对象线程的事件循环最终选取到假如没有事件循环运行事件不会分发给对象假如在一个线程中创建了一个QTimer对象但从没有调用过exec(),那么QTimer就不会发射它的timeout()信号deleteLater()也不会工作可以手工使用线程安全的函数QCoreApplication::postEvent()在任何时候给任何线程中的任何对象投递一个事件事件会在那个创建了对象的线程中通过事件循环派发事件过滤器在所有线程中也被支持不过它限定被监视对象与监视对象生存在同一线程中QCoreApplication::sendEvent(不是postEvent()),仅用于在调用此函数的线程中向目标对象投递事件

四、线程的同步

1、线程同步基础

    临界资源:每次只允许一个线程进行访问的资源
    线程间互斥:多个线程在同一时刻都需要访问临界资源
    线程锁能够保证临界资源的安全性通常每个临界资源需要一个线程锁进行保护
    线程死锁:线程间相互等待临界资源而造成彼此无法继续执行
    产生死锁的条件:
    A、系统中存在多个临界资源且临界资源不可抢占
    B、线程需要多个临界资源才能继续执行
    死锁的避免:
    A、对使用的每个临界资源都分配一个唯一的序号
    B、对每个临界资源对应的线程锁分配相应的序号
    C、系统中的每个线程按照严格递增的次序请求临界资源
    QMutex, QReadWriteLock, QSemaphore, QWaitCondition 提供了线程同步的手段使用线程的主要想法是希望它们可以尽可能并发执行而一些关键点上线程之间需要停止或等待例如假如两个线程试图同时访问同一个全局变量结果可能不如所愿

2、互斥量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;
     }
     mutex.unlock();
     return retVal;
 }

3、互斥锁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);
互斥锁加锁:    从声明处开始(在构造函数中加锁)
互斥锁解锁:    出了作用域自动解锁(在析构函数中解锁)

 int complexFunction(int flag)
     QMutexLocker locker(&mutex);
             if (status < 0)
         if (flag > 10)

4、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();

5、QReadLocker和QWriteLocker

    在较复杂的函数和异常处理中对QReadWriterLock类lock对象进行lockForRead()/lockForWrite()和unlock()操作将会很复杂进入点要lockForRead()/lockForWrite()在所有跳出点都要unlock()很容易出现在某些跳出点未调用unlock()所以Qt引进了QReadLocker和QWriteLocker类来简化解锁操作在函数需要的地方建立QReadLocker或QWriteLocker对象并把lock指针传给QReadLocker或QWriteLocker对象此时lock已经加锁等到退出函数后QReadLocker或QWriteLocker对象局部变量会自己销毁此时lock解锁

 QByteArray readData()
     ...
     return data;

使用QReadLocker:

 QReadLocker locker(&lock);

6、信号量QSemaphore

    QSemaphore 是QMutex的一般化是特殊的线程锁允许多个线程同时访问临界资源而一个QMutex只保护一个临界资源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 <cstdlib>
#include <cstdio>
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
QSemaphore  production(BufferSize);
QSemaphore  consumption;
class Producor:public QThread
    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
void Consumer::run()
        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 资源数-1consumer当前资源数=0

7、等待条件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 <QWaitCondition>
#include <QMutex>
#include <QTime>
const int DataSize = 32;
const int BufferSize = 16;
QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
int used = 0;
    qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
        mutex.lock();
        if(used == BufferSize)
            bufferNotFull.wait(&mutex);
        mutex.unlock();
        buffer[i%BufferSize] = used;
        used++;
        bufferNotEmpty.wakeAll();
        if(used == 0)
            bufferNotEmpty.wait(&mutex);
        fprintf(stderr, "%d\n", buffer[i%BufferSize]);
        used--;
        bufferNotFull.wakeAll();

8、高级事件队列

QT事件系统对进程间通信很重要每个进程可以有自己的事件循环要在另外一个线程中调用一个槽函数(或任何invokable方法)需要将调用槽函数放置在目标线程的事件循环中让目标线程在槽函数开始运行之前先完成自己的当前任务而原来的线程继续并行运行

要在一个事件循环中执行调用槽函数需要一个queued信号槽连接每当信号发出时信号的参数将被事件系统记录信号接收者存活的线程将运行槽函数另外不使用信号调用QMetaObject::invokeMethod()也可以达到相同的效果在这两种情况下必须使用queued连接因为direct连接绕过了事件系统并且立即在当前线程中运行此方法

    当线程同步使用事件系统时没有死锁风险然而事件系统不执行互斥如果调用方法访问共享数据仍然需要使用QMutex来保护

如果只使用信号槽并且线程间没有共享变量那么多线程程序可以完全没有低级原语

五、可重入与线程安全

可重入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)和网络类都不是线程安全的

 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函数

六、线程与信号槽

1、线程的依附性

    线程的依附性是对象与线程的关系默认情况下对象依附于自身被创建的线程
    对象的依附性与槽函数执行的关系默认情况下槽函数在其所依附的线程中被调用执行
    修改对象的依附性的方法:QObject::moveToThread函数用于改变对象的线程依附性使得对象的槽函数在依附的线程中被调用执行

2、QObject与线程

QThread类具有发送信号和定义槽函数的能力QThread主要信号如下:

  • void started();线程开始运行时发送信号
  • void finished();线程完成运行时发送信号
  • void terminated();线程被异常终止时发送信号

    QThread继承自QObject,发射信号以指示线程执行开始与结束并提供了许多槽函数QObjects可以用于多线程发射信号以在其它线程中调用槽函数并且向“存活”于其它线程中的对象发送事件

QObject的可重入性

    QObject是可重入的QObject的大多数非GUI子类如 QTimer、QTcpSocket、QUdpSocket、QHttp、QFtp、QProcess也是可重入的在多个线程中同时使用这些类是可能的可重入的类被设计成在一个单线程中创建与使用在一个线程中创建一个对象而在另一个线程中调用该对象的函数不保证能行得通有三种约束需要注意:

    A、一个QObject类型的孩子必须总是被创建在它的父亲所被创建的线程中这意味着除了别的以外永远不要把QThread对象(this)作为该线程中创建的一个对象的父亲(因为QThread对象自身被创建在另外一个线程中)
    B、事件驱动的对象可能只能被用在一个单线程中特别适用于计时器机制(timer mechanism)和网络模块例如:不能在不属于这个对象的线程中启动一个定时器或连接一个socket必须保证在删除QThread之前删除所有创建在这个线程中的对象在run()函数的实现中通过在栈中创建这些对象可以轻松地做到这一点
    C、虽然QObject是可重入的但GUI类尤其是QWidget及其所有子类都不是可重入的只能被用在GUI线程中QCoreApplication::exec()必须也从GUI线程被调用

    在实践中只能在主线程而非其它线程中使用GUI的类可以很轻易地被解决:将耗时操作放在一个单独的工作线程中当工作线程结束后在GUI线程中由屏幕显示结果

    一般来说在QApplication前创建QObject是不行的会导致奇怪的崩溃或退出取决于平台因此不支持QObject的静态实例一个单线程或多线程的应用程序应该先创建QApplication并最后销毁QObject

3、线程的事件循环

    每个线程都有自己的事件循环主线程通过QCoreApplication::exec()来启动自己的事件循环,但对话框的GUI应用程序有些时候用QDialog::exec()其它线程可以用QThread::exec()来启动事件循环就像 QCoreApplicationQThread提供一个exit(int)函数和quit()槽函数

    线程中的事件循环使得线程可以利用一些非GUI的、要求有事件循环存在的Qt类(例如:QTimer、QTcpSocket、和QProcess)使得连接一些线程的信号到一个特定线程的槽函数成为可能

    一个QObject实例被称为存活于它所被创建的线程中关于这个对象的事件被分发到该线程的事件循环中可以用QObject::thread()方法获取一个QObject所处的线程

    QObject::moveToThread()函数改变一个对象和及其子对象的线程所属性(如果对象有父对象的话对象不能被移动到其它线程中)

从另一个线程(不是QObject对象所属的线程)对该QObject对象调用delete方法是不安全的除非能保证该对象在那个时刻不处理事件使用QObejct::deleteLater()更好一个DeferredDelete类型的事件将被提交(posted)而该对象的线程的 件循环最终会处理这个事件默认情况下拥有一个QObject的线程就是创建QObject的线程而不是 QObject::moveToThread()被调用后的

    如果没有事件循环运行事件将不会传递给对象例如:在一个线程中创建了一个QTimer对象但从没有调用exec()那么QTimer就永远不会发射timeout()信号即使调用deleteLater()也不行(这些限制也同样适用于主线程)

    利用线程安全的方法QCoreApplication::postEvent()可以在任何时刻给任何线程中的任何对象发送事件事件将自动被分发到该对象所被创建的线程事件循环中

    所有的线程都支持事件过滤器而限制是监控对象必须和被监控对象存在于相同的线程中QCoreApplication::sendEvent()(不同于postEvent())只能将事件分发到和该函数调用者相同的线程中的对象

4、其他线程访问QObject子类

    QObject及其所有子类都不是线程安全的这包含了整个事件交付系统重要的是切记事件循环可能正在向你的QObject子类发送事件当你从另一个线程访问该对象时
    如果你正在调用一个QObject子类的函数而该子类对象并不存活于当前线程中并且该对象是可以接收事件的那么你必须用一个mutex保护对该QObject子类的内部数据的所有访问否则就有可能发生崩溃和非预期的行为
    同其它对象一样QThread对象存活于该对象被创建的线程中 – 而并非是在QThread::run()被调用时所在的线程一般来说在QThread子类中提供槽函数是不安全的除非用一个mutex保护成员变量
    另一方面可以在QThread::run()的实现中安全地发射信号因为信号发射是线程安全的

5、跨线程的信号槽

    线程的信号槽机制需要开启线程的事件循环机制即调用QThread::exec()函数开启线程的事件循环
Qt信号-槽连接函数原型如下:

bool QObject::connect ( const QObject * sender, const char * signal, const QObject * receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection ) 

Qt支持5种连接方式

A、Qt::DirectConnection(直连方式)(信号与槽函数关系类似于函数调用同步执行)
    当信号发出后相应的槽函数将立即被调用emit语句后的代码将在所有槽函数执行完毕后被执行
    当信号发射时槽函数将直接被调用
    无论槽函数所属对象在哪个线程槽函数都在发射信号的线程内执行
B、Qt::QueuedConnection(队列方式)(此时信号被塞到事件队列里信号与槽函数关系类似于消息通信异步执行)
    当信号发出后排队到信号队列中需等到接收对象所属线程的事件循环取得控制权时才取得该信号调用相应的槽函数emit语句后的代码将在发出信号后立即被执行无需等待槽函数执行完毕
    当控制权回到接收者所依附线程的事件循环时槽函数被调用
    槽函数在接收者所依附线程执行
C、Qt::AutoConnection(自动方式)
     Qt的默认连接方式如果信号的发出和接收信号的对象同属一个线程那个工作方式与直连方式相同;否则工作方式与队列方式相同
如果信号在接收者所依附的线程内发射则等同于直接连接
如果发射信号的线程和接受者所依附的线程不同则等同于队列连接
D、Qt::BlockingQueuedConnection(信号和槽必须在不同的线程中否则就产生死锁)
    槽函数的调用情形和Queued Connection相同不同的是当前的线程会阻塞住直到槽函数返回
E、Qt::UniqueConnection
    与默认工作方式相同只是不能重复连接相同的信号和槽因为如果重复连接就会导致一个信号发出对应槽函数就会执行多次

    QThread是用来管理线程的QThread对象所依附的线程和所管理的线程并不是同一个概念QThread所依附的线程就是创建QThread对象的线程QThread 所管理的线程就是run启动的线程也就是新建线程QThread对象依附在主线程中QThread对象的slot函数会在主线程中执行而不是次线程除非QThread对象依附到次线程中(通过movetoThread)
工程实践中为了避免冻结主线程的事件循环(即避免因此而冻结了应用的UI)所有的计算工作是在一个单独的工作线程中完成的工作线程结束时发射一个信号通过信号的参数将工作线程的状态发送到GUI线程的槽函数中更新GUI组件状态

七、线程的设计

1、线程的生命周期

如果线程的正处于执行过程中时线程对象被销毁时程序将会出错
工程实践中线程对象的生命期必须大于线程的生命期

2、同步线程类设计

线程对象主动等待线程生命期结束后才销毁线程对象销毁时确保线程执行结束支持在栈或堆上创建线程对象
在线程类的析构函数中先调用wait函数强制等待线程执行结束
使用场合:适用于线程生命期较短的场合

#ifndef SYNCTHREAD_H
#define SYNCTHREAD_H
class SyncThread : public QThread
  Q_OBJECT
  explicit SyncThread(QObject* parent = 0):QThread(parent)
  ~SyncThread()
    wait();
#endif // SYNCTHREAD_H

3、异步线程类设计

线程生命期结束时通知线程对象销毁
只能在堆空间创建线程对象线程对象不能被外界主动销毁
在run函数中最后调用deleteLater()函数
线程函数主动申请销毁线程对象
使用场合:
线程生命期不可控需要长时间运行于后台的线程

#ifndef ASYNCTHREAD_H
#define ASYNCTHREAD_H
class AsyncThread : public QThread
    deleteLater();
  explicit AsyncThread(QObject* parent = 0):QThread(parent)
  ~AsyncThread()
  static AsyncThread* newThread(QObject* parent = 0)
    return new AsyncThread(parent);
#endif // ASYNCTHREAD_H

八、线程的使用方式

1、子类化QThread

QThread的两种使用方法:
(1)不使用事件循环
 A、子类化 QThread
    B、重写run函数run函数内有一个 while 或 for 的死循环
    C、设置一个标记为来控制死循环的退出
    适用于后台执行长时间的耗时操作如文件复制、网络数据读取
(2)使用事件循环
    A、子类化 QThread
    B、重写run 使其调用 QThread::exec() 开启线程的事件循环
C、为子类定义信号和槽由于槽函数并不会在新开的 Thread 运行在构造函数中调用 moveToThread(this)
适用于事务性操作如文件读写、数据库读写

2、Worker-Object

    在Qt4.4之前run 是纯虚函数必须子类化QThread来实现run函数
    而从Qt4.4开始QThread不再支持抽象类run 默认调用 QThread::exec() 不需要子类化 QThread只需要子类化一个 QObject 
    通过继承的方式实现多线程已经没有任何意义QThread是操作系统线程的接口或控制点用于充当线程操作的集合
    使用Worker-Object通过QObject::moveToThread将它们移动到线程中
    指定一个线程对象的线程入口函数的方法:
A、在类中定义一个槽函数void tmain()作为线程入口函数
B、在类中定义一个QThread成员对象m_thread
C、改变当前对象的线程依附性到m_thread
D、连接m_thread的started()信号到tmain槽函数

#ifndef WORKER_H
#define WORKER_H
#include <QObject>
class Worker : public QObject
  QThread m_thread;
protected slots:
  void tmain()
    qDebug() << "void tmain()";
  explicit Worker(QObject* parent = 0):QObject(parent)
    moveToThread(&m_thread);
    connect(&m_thread, SIGNAL(started()), this, SLOT(tmain()));
  void start()
    m_thread.start();
  void terminate()
    m_thread.terminate();
  void exit(int c)
    m_thread.exit(c);
  ~Worker()
    m_thread.wait();
#endif // WORKER_H

九、多线程与GUI组件的通信

1、多线程与GUI组件通信基础

    GUI系统的设计原则:
    所有界面组件的创建只能在GUI线程(主线程)中完成子线程与界面组件的通信有两种方式:
    A、信号槽方式
    B、发送自定事件方式

2、信号槽方式

使用信号槽解决多线程与界面组件的通信的方案:
A、在子线程中定义界面组件的更新信号
B、在主窗口类中定义更新界面组件的槽函数
C、使用异步方式连接更新信号到槽函数
子线程通过发送信号的方式更新界面组件所有的界面组件对象只能依附于GUI线程(主线程)
子线程更新界面状态的本质是子线程发送信号通知主线程界面更新请求主线程根据具体信号以及信号参数对界面组件进行修改
使用信号槽在子线程中更新主界面中进度条的进度显示信息
工作线程类:

signals:
  void signalProgressValue(int value);
    work();
    exec();
    moveToThread(this);
  void work()
    for(int i = 0; i < 11; i++)
        emit signalProgressValue(i*10);
        sleep(1);

主界面类:

#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QProgressBar>
#include "WorkThread.h"
class Widget : public QWidget
  QProgressBar* m_progress;//进度条
  WorkThread* m_thread;//工作线程
  Widget(QWidget *parent = 0):QWidget(parent)
    m_progress = new QProgressBar(this);
    m_progress->move(10, 10);
    m_progress->setMinimum(0);
    m_progress->setMaximum(100);
    m_progress->setTextVisible(true);
    m_progress->resize(100, 30);
    m_thread = new WorkThread();
    m_thread->start();
    connect(m_thread, SIGNAL(finished()), m_thread, SLOT(deleteLater()));
    //连接工作线程的信号到界面的槽函数
    connect(m_thread, SIGNAL(signalProgressValue(int)), this, SLOT(onProgress(int)));
  ~Widget()
  void onProgress(int value)
    m_progress->setValue(value);
#endif // WIDGET_H

Main函数:

#include "Widget.h"
#include <QApplication>
  QApplication a(argc, argv);
  Widget w;
  w.show();
  return a.exec();

3、发送自定义事件方式

    A、自定义事件用于描述界面更新细节
    B、在主窗口类中重写事件处理函数event
    C、使用postEvent函数(异步方式)发送自定义事件类对象
    子线程指定接收消息的对象为主窗口对象在event事件处理函数更新界面状态
    事件对象在主线程中被处理event函数在主线程中调用
    发送的事件对象必须在堆空间创建
    子线程创建时必须附带目标对象的地址信息
自定义事件类:

#ifndef PROGRESSEVENT_H
#define PROGRESSEVENT_H
#include <QEvent>
class ProgressEvent : public QEvent
  int m_progress;
  const static Type TYPE = static_cast<Type>(QEvent::User + 0xFF);
  ProgressEvent(int progress = 0):QEvent(TYPE)
    m_progress = progress;
  int progress()const
    return m_progress;
#endif // PROGRESSEVENT_H

自定义线程类:

#include <ProgressEvent.h>
        QApplication::postEvent(parent(), new ProgressEvent(i*10));

自定义界面类:

#ifndef WIDGETUI_H
#define WIDGETUI_H
#include "ProgressEvent.h"
class WidgetUI : public QWidget
  WidgetUI(QWidget *parent = 0):QWidget(parent)
    m_thread->setParent(this);
  ~WidgetUI()
    m_thread->quit();
  bool event(QEvent *event)
    bool ret = true;
    if(event->type() == ProgressEvent::TYPE)
        ProgressEvent* evt = dynamic_cast<ProgressEvent*>(event);
        if(evt != NULL)
            //设置进度条的进度为事件参数的值
            m_progress->setValue(evt->progress());
    else
        ret = QWidget::event(event);
    return ret;
#endif // WIDGETUI_H
#include "WidgetUI.h"
  WidgetUI w;

相关文章

猜您喜欢

网友评论

Copyright 2020 www.gamerfx.net 【游戏天空】 版权所有 软件发布

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 点此查看联系方式