背景引言[ 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 使用步骤:
- 创建一个继承于QThread线程类的子类MyThread,即派生QThread;
- 重写MyThread类中线程任务函数run () 方法,在该函数内编写子线程要处理的具体的业务流程,线程入口;
- 在主线程中创建MyThread子线程对象,调用 start () 方法就启动MyThread子线程;
2.1.2 注意事项:
- 不能在类的外部调用 run () 方法启动子线程,在外部调用 start () 相当于让 run () 开始运行
- 在 Qt 中在子线程中不要操作程序中的窗口类型对象,不允许,如果操作了程序就挂了
- 只有主线程才能操作程序中的窗口对象,默认的线程就是主线程,自己创建的就是子线程
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函数为子线程,分处不同的线程中
- 自动连接(AutoConnection):默认的连接方式。如果信号与槽,也就是发送者与接受者在同一线程,等同于直接连接;如果发送者与接受者处在不同线程,等同于队列连接。
- 直接连接(DirectConnection):当信号发射时,槽函数将直接被调用。无论槽函数所属对象在哪个线程,槽函数都在发射者所在线程执行。
- 队列连接(QueuedConnection):当控制权回到接受者所在线程的事件循环式,槽函数被调用。槽函数在接收者所在线程执行。
- 阻塞队列连接(BlockingQueuedConnection):信号和槽必须在不同的线程中,否则就产生死锁,槽函数的调用情形和Queued Connection相同,不同的是当前的线程会阻塞住,直到槽函数返回【emit后阻塞执行,直到slot执行完毕后,才执行emit后续代码】
- 唯一连接(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也是可重入的,在多个线程中同时使用这些类是可能的。可重入的类被设计成在一个单线程中创建与使用,在一个线程中创建一个对象而在另一个线程中调用该对象的函数,不保证能行得通。有三种约束需要注意:
- QObject实例必须被创建在它父类所被创建的线程中。这意味着,一般情况下永远不要把QThread对象(this)作为该线程中创建的一个对象的父亲(因为QThread对象自身被创建在另外一个线程中,即 QThread* t =new QThread,不要(this))。
- 事件驱动的对象可能只能被用在一个单线程中。特别适用于计时器机制(timer mechanism)和网络模块。例如:不能在不属于这个对象的线程中启动一个定时器或连接一个socket,必须保证在删除QThread之前删除所有创建在这个线程中的对象(thread中创建的对象需要在线程释放前释放改对象)。在run()函数的实现中,通过在栈中创建这些对象,可以轻松地做到这一点。
- 虽然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组件状态。
标签:Qt,对象,创建,QObject,线程,多线程,QThread,函数 From: https://www.cnblogs.com/david-china/p/17106504.html