Qt多线程编程之QThread

背景引言[ GUI主线程 +子线程]

  跟C++11中很像的是,Qt中使用QThread来管理线程,一个QThread对象管理一个线程,在使用上有很多跟C++11中相似的地方,但更多的是Qt中独有的内容。另外,QThread对象也有消息循环exec()函数,即每个线程都有一个消息循环,用来处理自己这个线程的事件。

  QCoreApplication::exec()总是在主线程(执行main()的线程)中被调用,在GUI程序中,主线程也称为GUI线程,是唯一允许执行GUI相关操作的线程,所有要创建的其他线程任务都要依附于主线程。因此,若要创建一个QThread线程任务,前提是必须创建QApplication(or QCoreApplication)对象。

  GUI应用程序开发的时候, 假设应用程序在某些情况下需要处理比较复杂的逻辑, 如果只有一个线程去处理,就会导致窗口卡顿,无法处理用户的相关操作。这种情况下就需要使用多线程,其中一个线程处理窗口事件,其他线程进行逻辑运算,多个线程各司其职,不仅可以提高用户体验还可以提升程序的执行效率。

  在 qt 中使用了多线程,有些事项是需要额外注意:

  • 默认的线程在Qt中称之为窗口线程,也叫主线程即GUI线程,负责窗口事件处理或者窗口控件数据的更新
  • 子线程负责后台的业务逻辑处理,子线程中不能对窗口对象做任何操作,这些事情需要交给窗口线程处理
  • 主线程和子线程之间如果要进行数据的传递,需要使用Qt中的信号槽机制

一、QThread 线程类 

Qt 中提供了一个QThread 线程类【继承于QObject,区别于QRunnable】,通过这个类就可以创建子线程了。

1.1 常用成员函数

// 构造函数,父类QObject 
QThread::QThread(QObject *parent = Q_NULLPTR);
// 判断线程中的任务是不是处理完毕了
bool QThread::isFinished() const;
// 判断子线程是不是在执行任务
bool QThread::isRunning() const;

// 获取、设置线程的优先级
Priority QThread::priority() const;
void QThread::setPriority(Priority priority);
优先级:
    QThread::IdlePriority         --> 最低的优先级
    QThread::LowestPriority
    QThread::LowPriority
    QThread::NormalPriority
    QThread::HighPriority
    QThread::HighestPriority
    QThread::TimeCriticalPriority --> 最高的优先级
    QThread::InheritPriority      --> 子线程和其父线程的优先级相同, 默认是这个
// 退出线程, 停止底层的事件循环
// 退出线程的工作函数
void QThread::exit(int returnCode = 0);
// 调用线程退出函数之后, 线程不会马上退出因为当前任务有可能还没有完成, 调回用这个函数是
// 等待任务完成, 然后退出线程, 一般情况下会在 exit() 后边调用这个函数
bool QThread::wait(unsigned long time = ULONG_MAX);

1.2 信号槽

//等同于exit() 效果,之后也要调 wait() 函数
[slot] void QThread::quit();
// 启动子线程
[slot] void QThread::start(Priority priority = InheritPriority);

// 函数用于强制结束线程,不保证数据完整性和资源释放,慎用
[slot] void QThread::terminate();

// 线程中执行完任务后, 发出该信号
[signal] void QThread::finished();
// 开始工作之前发出这个信号, 一般不使用
[signal] void QThread::started();

1.3 静态函数

// 当前执行线程的QThread指针对象
[static] QThread *QThread::currentThread();
// 返回系统上运行的理想线程数 == 和当前电脑的 CPU 核心数相同
[static] int QThread::idealThreadCount();
// 线程休眠函数
[static] void QThread::msleep(unsigned long msecs);    // 单位: 毫秒
[static] void QThread::sleep(unsigned long secs);    // 单位: 秒
[static] void QThread::usleep(unsigned long usecs);    // 单位: 微秒

1.4 任务处理函数

// 子线程要处理什么任务, 需要写到 run() 中
[virtual protected] void QThread::run();
//线程的起点,在调用start()之后,新创建的线程就会调用run函数,默认实现调用exec(),run函数返回时,线程的执行将结束。

二、QThread的两种方法

2.1 派生QThread类对象的方法(重写Run函数)

2.1.1 使用步骤:

  1. 创建一个继承于QThread线程类的子类MyThread,即派生QThread;
  2. 重写MyThread类中线程任务函数run () 方法,在该函数内编写子线程要处理的具体的业务流程,线程入口;
  3. 在主线程中创建MyThread子线程对象,调用 start () 方法就启动MyThread子线程;

2.1.2 注意事项:

  1. 不能在类的外部调用 run () 方法启动子线程,在外部调用 start () 相当于让 run () 开始运行
  2. 在 Qt 中在子线程中不要操作程序中的窗口类型对象不允许,如果操作了程序就挂了
  3. 只有主线程才能操作程序中的窗口对象,默认的线程就是主线程,自己创建的就是子线程

2.1.3 例子:

  尝试用多线程实现10s耗时的操作:(用按钮触发)

线程类workThread :

//workThread .h
class workThread : public QThread
{
public:
    void run();
};

//workThread.cpp
workThread::workThread(QObject* parent)
{}
//线程入口:主要处理的后台业务逻辑或数据更新等
void workThread::run()
{
    qDebug() << "当前子线程ID:" << QThread::currentThreadId();
    qDebug() << "开始执行线程";
    QThread::sleep(10);
    qDebug() << "线程结束";
}

窗口主线程中启用子线程:

//Threadtest .h
class Threadtest : public QMainWindow
{
    Q_OBJECT

public:
    Threadtest(QWidget *parent = Q_NULLPTR);

private:
    Ui::ThreadtestClass ui;
    void btn_clicked();
    workThread* thread;
};

//threadtest.cpp
Threadtest::Threadtest(QWidget* parent)
    : QMainWindow(parent)
{
    ui.setupUi(this);
    connect(ui.btn_start, &QPushButton::clicked, this, &Threadtest::btn_clicked);
    thread = new workThread ; //主线程中创建workThread子线程对象,
} 

void Threadtest::btn_clicked()
{
  qDebug()
<< "主线程id:" << QThread::currentThreadId();
  thread
->start();//启动子线程
}

2.2.moveToThread+槽函数链接绑定线程接口

使用QThread派生类对象的方法创建线程,这种方法存在一个局限性,假设要在一个子线程中处理多个任务,所有的处理逻辑都需要写到run()函数中,这样该函数中的处理逻辑就会变得非常混乱,不太容易维护。所以,Qt 提供的第二种线程的创建方式弥补了第一种方式的缺点,用起来更加灵活,就是使用信号与槽的方式,即把在线程中执行的函数(我们可以称之为线程函数)定义为一个槽函数。

2.2.1 使用步骤

  • 创建一个新的类(mywork),让这个类从 QObject 派生,在这个类中添加一个公共的成员函数(working),函数体就是我们要子线程中执行的业务逻辑
  • 在主线程中创建一个 QThread 对象,这就是子线程的对象
  • 在主线程中创建工作的类对象(千万不要指定给创建的对象指定父对象)
  • 将 MyWork 对象移动到创建的子线程对象中,需要调用 QObject 类提供的 moveToThread() 方法
  • 启动子线程,调用 start(), 这时候线程启动了,但是移动到线程中的对象并没有工作
  • 调用 MyWork 类对象的工作函数,让这个函数开始执行,这时候是在移动到的那个子线程中运行的

2.2.2 代码样例

//MyWork .h
class MyWork : public QObject
{
    Q_OBJECT
public:
    explicit MyWork(QObject *parent = nullptr);
    // 工作函数
    void working();
signals:
    void curNumber(int num);
public slots:
};

//mywork.cpp
MyWork::MyWork(QObject *parent) : QObject(parent)
{}
void MyWork::working()
{
    qDebug() << "当前线程对象的地址: " << QThread::currentThread();
    int num = 0;
    while(1)
    {
        emit curNumber(num++);
        if(num == 10000000)
            break;
        QThread::usleep(1);
    }
    qDebug() << "run() 执行完毕, 子线程退出...";
}


//主程序
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    qDebug() << "主线程对象的地址: " << QThread::currentThread();
    // 创建线程对象
    QThread* sub = new QThread ;
    // 创建工作的类对象
    // 千万不要指定给创建的对象指定父对象
    // 如果指定了: QObject::moveToThread: Cannot move objects with a parent
    MyWork* work = new MyWork;
    // 将工作的类对象移动到创建的子线程对象中
    work->moveToThread(sub);
    // 启动线程
    sub->start();
    // 让工作的对象开始工作, 点击开始按钮, 开始工作
    connect(ui->startBtn, &QPushButton::clicked, work, &MyWork::working);//signal对象为主线程,slot函数为子线程,分处不同的线程中
    // 显示数据
    connect(work, &MyWork::curNumber, this, [=](int num)
    {
        ui->label->setNum(num);
    });
}

MainWindow::~MainWindow()
{
    delete ui;
}

2.2.3 注意事项

使用这种多线程方式,假设有多个不相关的业务流程需要被处理,那么就可以创建多个类似于 MyWork 的类,将业务流程放多类的公共成员函数中,然后将这个业务类的实例对象移动到对应的子线程中 moveToThread() 就可以了,这样可以让编写的程序更加灵活,可读性更强,更易于维护。使用时注意事项一下:

  • 子线程中不要操作UI:Qt创建的子线程中是不能对UI对象进行任何操作的,即QWidget及其派生类对象。Qt中子线程不能执行任何关于界面的处理,正确的操作应该是通过信号槽,将一些参数传递给主线程,让主线程(也就是Controller)去处理。
  • 对任务类进行声明初始化时,不要指定父对象:比如上面程序中的:MyWork* work = new MyWork;
  • 跨线程的信号槽:QThread与connect的关系中在使用connect函数的时候,我们一般会把最后一个参数忽略掉,即第五个参数。最后一个参数代表的是连接的方式:
     connect(ui->startBtn, &QPushButton::clicked, work, &MyWork::working);//signal对象为主线程slot函数为子线程,分处不同的线程中

 

  1. 自动连接(AutoConnection):默认的连接方式。如果信号与槽,也就是发送者与接受者在同一线程,等同于直接连接;如果发送者与接受者处在不同线程,等同于队列连接
  2. 直接连接(DirectConnection):当信号发射时,槽函数将直接被调用。无论槽函数所属对象在哪个线程,槽函数都在发射者所在线程执行
  3. 队列连接(QueuedConnection):当控制权回到接受者所在线程的事件循环式,槽函数被调用。槽函数在接收者所在线程执行
  4. 阻塞队列连接(BlockingQueuedConnection):信号和槽必须在不同的线程中,否则就产生死锁,槽函数的调用情形和Queued Connection相同,不同的是当前的线程会阻塞住,直到槽函数返回【emit后阻塞执行,直到slot执行完毕后,才执行emit后续代码】
  5. 唯一连接(UniqueConnection):是配合前四种使用的。确保相同的信号,相同的槽保持唯一连接。作用就是使相同信号唯一连接相同槽。但是你在下一次连接的时候,如果不使用Qt::UniqueConnection,下次连接还是会成功,不会使唯一连接生效。要两次都使用Qt::UniqueConnection,才会生效。

 

 

三、 Qt中线程安全问题

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

3.1 线程同步

3.1.1 线程同步基础概念

  •     临界资源:每次只允许一个线程进行访问的资源
  •     线程间互斥:多个线程在同一时刻都需要访问临界资源

    线程锁能够保证临界资源的安全性,通常,每个临界资源需要一个线程锁进行保护

    线程死锁:线程间相互等待临界资源而造成彼此无法继续执行。

    产生死锁的条件:

  • 系统中存在多个临界资源且临界资源不可抢占
  • 线程需要多个临界资源才能继续执行

QMutex, QReadWriteLock, QSemaphore, QWaitCondition 提供了线程同步的手段。使用线程的主要想法是希望它们可以尽可能并发执行,而一些关键点上线程之间需要停止或等待

3.1.2 互斥锁QMutex、QMutexLocker、QWaitCondition

  • QMutex 提供相互排斥的锁,或互斥量。在一个时刻至多一个线程拥有mutex,假如一个线程试图访问已经被锁定的mutex,那么线程将休眠,直到拥有mutex的线程对此mutex解锁,QMutex常用来保护共享数据访问,如果使用了Mutex.lock()而没有对应的使用Mutex.unlcok()的话就会造成死锁,其他的线程将永远也得不到接触Mutex锁住的共享资源的机会;QMutexLocker类似于c++中std::mutex
  • 在较复杂的函数和异常处理中对QMutex类mutex对象进行lock()和unlock()操作将会很复杂,而且开销也大,所以Qt引进了QMutex的辅助类QMutexLocker来避免lock()和unlock()操作。在函数需要的地方建立QMutexLocker对象,并把mutex指针传给QMutexLocker对象,此时mutex已经加锁,等到退出函数后,QMutexLocker对象局部变量会自己销毁,此时mutex解锁。QMutexLocker类似于c++中std::lock_guard<>
  • QWaitCondition 允许线程在某些情况发生时唤醒另外的线程。一个或多个线程可以阻塞等待QWaitCondition , 用wakeOne()或wakeAll()设置一个条件。wakeOne()随机唤醒一个,wakeAll()唤醒所有。QWaitCondition 类似于c++中condition_variable

3.2 QObject的可重入性问题【QTcpsocket中使用多线程技术

一个线程安全的函数可以同时被多个线程调用,甚至调用者会使用共享数据也没有问题,因为对共享数据的访问是串行的。一个可重入函数也可以同时被多个线程调用,但是每个调用者只能使用自己的数据。因此,一个线程安全的函数总是可重入的,但一个可重入的函数并不一定是线程安全的。

    一个可重入的类,指的是类的成员函数可以被多个线程安全地调用,只要每个线程使用类的不同的对象。而一个线程安全的类,指的是类的成员函数能够被多线程安全地调用,即使所有的线程都使用类的同一个实例。

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

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

在实践中,只能在主线程而非其它线程中使用GUI的类,可以很轻易地被解决:将耗时操作放在一个单独的工作线程中,当工作线程结束后在GUI线程中由屏幕显示结果。一般来说,在QApplication前创建QObject是不行的,会导致奇怪的崩溃或退出,取决于平台。因此,不支持QObject的静态实例。一个单线程或多线程的应用程序应该先创建QApplication,并最后销毁QObject

3.3 线程的事件循环

  • 每个线程都有自己的事件循环。主线程通过QCoreApplication::exec()来启动自己的事件循环, 但对话框的GUI应用程序,有些时候用QDialog::exec(),其它线程可以用QThread::exec()来启动事件循环。就像 QCoreApplication,QThread提供一个exit(int)函数和quit()槽函数,这里要注意与wait()搭配使用
  • 信号槽机制让发射(发射线程)连接到接收线程中:的事件循环使得线程可以利用一些非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())只能将事件分发到和该函数调用者相同的线程中的对象。

 

工程实践中,为了避免冻结主线程的事件循环(即避免因此而冻结了应用的UI),所有的计算工作是在一个单独的工作线程中完成的,工作线程结束时发射一个信号,通过信号的参数将工作线程的状态发送到GUI线程的槽函数中更新GUI组件状态

原文链接: https://www.cnblogs.com/david-china/p/17106504.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    Qt多线程编程之QThread

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/317025

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月16日 下午2:30
下一篇 2023年2月16日 下午2:31

相关推荐