目标:了解Qt实现多线程的两种基本方式(QThread、moveToThread),各自的实现方式、使用场景。
1,子类化QThread
实现方式:继承QThread类,重写run()函数实现多线程class WorkerThread : public QThread
{
Q_OBJECT
public:
explicit MyThread(QObject *parent = nullptr) : QObject(parent), m_isStop(false) {}
void run() {
QString result;
while(!m_isStop) {
qDebug()<<"child thread"<<QThread::currentThreadId()<<endl;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
QThread::sleep(5);
}
}
void stop(){m_isStop = true;}
signals:
void resultReady(const QString &s);
private:
bool m_isStop;//停止标志位
};
void MyObject::startWorkInAThread()
{
WorkerThread *workerThread = new WorkerThread(this);
connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
workerThread->start();
}
默认情况下,run()通过调用exec()来启动事件循环,并在线程内运行 Qt 事件循环。必须调用QThread的quit()函数或者exit()函数才可以使子线程退出消息循环,并且有时还不是马上就退出,需要等到CPU的控制权交给线程的exec()。所以先要thread.quit();使退出子线程的消息循环, 然后thread.wait();在主线程中回收子线程的资源。应用场景适合相对简单独立运行的常驻或后台任务,不需要跨线程频繁通信的任务注意事项1,QThread 实例位于实例化它的旧线程中,而不是位于调用 run()的新线程中。2,run()函数作为子线程的入口,即子线程从run()开始执行。在子线程中完成的工作都要写在run()函数中。3,Thread(即创建的类)中的成员变量属于主线程,在访问前需要判断访问是否安全。
2,自定义类moveToThread
实现方式:定义QObject派生类,然后将其对象move到QThread中class Worker : public QObject
{
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr) : QObject(parent), m_isStop(false) {}
~Worker() { qDebug() << "Worker destroyed in thread" << QThread::currentThreadId(); }
void stop() { m_isStop = true; }
public slots:
void doWork(const QString ¶meter) {
QString result;
if(!m_isStop) {
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
}
}
signals:
void resultReady(const QString &result);
private:
bool m_isStop;
};
class Controller : public QObject
{
Q_OBJECT
QThread workerThread;
public:
Controller() {
//不要给对象指定父对象
Worker *worker = new Worker;
// 将Worker对象移动到新创建的线程中
worker->moveToThread(&workerThread);
connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
connect(this, &Controller::operate, worker, &Worker::doWork);
connect(worker, &Worker::resultReady, this, &Controller::handleResults);
connect(&workerThread, &QThread::started, worker, &Worker::doWork);
workerThread.start();// 开启线程
}
~Controller() {
workerThread.quit();// 等待线程结束
workerThread.wait();
}
public slots:
void handleResults(const QString &);
signals:
void operate(const QString &);
};
QThread应该被看做是操作系统线程的接口或控制点,而不应该包含需要在新线程中运行的代码。需要运行的代码应该放到一个QObject的子类中,然后将该子类的对象moveToThread到新线程中。采用moveToThread的方式,将槽函数所在对象移入子线程,发送信号函数在主线程,槽函数在单独的子线程。应用场景适合线程间频繁通信的任务注意事项1,自定义类对象不要指定父对象,否则提示:QObject::moveToThread: Cannot move objects with a parent2,把子线程的finished信号和自定义类对象的deleteLater槽连接,结束子线程则自动析构自定义类的对象。
综上所述1,常驻任务使用继承QThread重写run,线程逻辑相对独立,不需要与主线程频繁交互。2,其它的一次性任务或者间歇性任务尽量都用moveToThread,线程执行的任务与 Qt 的事件系统(如信号和槽)交互允许在不同的线程中运行任务,同时保持与主线程的通信。
3,QThread和connect的关系
信号与槽的连接函数的原型bool QObject::connect (const QObject * sender,
const char * signal,
const QObject * receiver,
const char * method,
Qt::ConnectionType type = Qt::AutoConnection)
其中第5个参数决定信号与槽的连接方式,用于决定槽函数被调用时的相关行为。Qt::AutoConnection 默认连接
Qt::DirectConnection 槽函数立即调用
Qt::BlockingQueuedConnection 同步调用
Qt::QueuedConnection 异步调用
Qt::UniqueConnection 单一连接
(1) Qt::DirectConnection(立即调用)直接在发送信号的线程中调用槽函数(无论发送信号和槽函数是否位于同一线程),等价于槽函数的实时调用。也就是说槽函数在发送信号所在线程调用。直接连接,其实就等同于直接调用。
(2) Qt::QueuedConnection(异步调用)信号发送至目标线程的事件队列(发送信号和槽函数位于不同线程),交由目标线程处理,当前线程继续向下执行。当信号发送时候,槽函数不会直接调用,直到接受者线程取得控制权时进行事件处理循环时候,槽函数才会调用执行。跨线程时只有队列连接是安全的,队列连接借助的是事件系统,所以你可以通过postEvent在线程间传递数据。
(3) Qt::BlockingQueuedConnection(同步调用)信号发送至目标线程的事件队列,由目标线程处理。当前线程阻塞等待槽函数的返回,之后向下执行。Qt::BlockingQueuedConnection 和QueuedConnectionx相同,但是sender发送后线程会进入阻塞状态,只有receiver线程执行槽函数完成,才会结束阻塞状态,所以这种参数类型设定情况下,sender和receiver不能在同一个线程,否则会造成死锁发生。
(4) Qt::AutoConnection(默认连接)当发送信号线程=槽函数线程时,效果等价于Qt::DirectConnection;当发送信号线程!=槽函数线程时,效果等价于Qt::QueuedConnection。Qt::AutoConnection是connect()函数第5个参数的默认值,也是实际开发中字常用的连接方式。
(5) Qt::UniqueConnection(单一连接)功能和AutoConnection相同,同样能自动确定连接类型,但是加了限制:同一个信号和同一个槽函数之间只能有一个连接。
注意:如果槽函数工作在receiver线程(不和sender在同一个线程),并且槽函数中有耗时操作,比如while循环等,这个时候sender在发送信号,槽函数是不会响应的,除非槽函数工作在sender线程中,也就是要把参数设置为DirectConnection或者槽函数所在线程开启事件循环。
4,多线程开发总结
4.1 确保线程安全多线程程序需要确保线程安全,特别是在多个线程共享数据时更为重要。需要使用互斥锁、信号槽等机制来协调线程之间的访问。同时,需要避免在不同线程中对同一对象进行相互矛盾的操作,以避免出现死锁等问题。4.2 遵循对象树和内存管理原则在Qt中,使用多线程时需要遵循对象树和内存管理原则。通常情况下,QObject对象只能在创建它的线程中使用。如果需要在其他线程中使用,可以通过使用moveToThread方法将其移动到其他线程中。同时,需要注意对象的创建和销毁时机,避免出现内存泄漏等问题。
4.3 选择合适的多线程方式在使用多线程时,需要选择合适的多线程方式。通常建议使用自定义类并使用moveToThread方法的方式,而不是继承QThread的方式。这样可以更好地管理线程的生命周期和对象树,同时也更易于实现线程安全和单元测试。
4.4 控制线程数目在使用多线程时需要控制线程数目,避免过多的线程导致资源浪费和性能下降。可以通过Qt提供的QThreadPool类来管理线程池,从而更好地控制线程数目。
4.5 避免阻塞主线程在使用多线程时,需要注意避免阻塞主线程。长时间的计算或IO操作应该在子线程中完成,以便主线程能够更快地响应用户操作。同时,也需要注意信号槽的连接方式,避免连接方式不当导致阻塞主线程。
4.6 子线程中不能操作UI控件Qt创建的子线程中是不能对UI对象进行任何操作的,即QWidget及其派生类对象。每个程序在启动的时候都有一个线程,这个线程被称为“主线程”(在Qt应用里就是GUI线程)。Qt的GUI必须运行在这个线程上。所有的窗口和几个相关的类,例如QPixmap,不可以在另一个线程上运行(主要是事件循环在主线程里,窗口的绘制事件,用户的输入事件等等只会在事件事件循环中处理)。其他线程一般当作工作线程来使用,可以用来减少主线程的负担,处理一些其他的工作。
4.7 安全的结束子线程从Qt 4.8开始,可以通过将finish()信号连接到QObject::deleteLater()来释放存在于刚刚结束的线程中的对象。可以通过调用 exit()或 quit()来停止线程。可以使用 isDone()和 isRunning()来查询线程的状态。
建议使用的方法是在每次循环之前进行一个BOOL值的判断,
当BOOL值为假时,退出循环(当然,也可以使用terminate()粗暴的结束线程)。
当run函数里面没有循环时,函数像普通函数一样,运行完一次即退出函数。
线程结束时会发出信号,此时我们可以通过信号槽来销毁线程:
connect(thread,&QThread::finished
,thread,&QObject::deleteLater);//线程结束后调用deleteLater来销毁分配的内存
善用QObject::deleteLater 和 QObject::destroyed来进行内存管理在new对象时候,直接用this指定其父类(即放入对象数中)
在程序最后自行释放资源
connect(this, &list::destroyed, this, [=]() {
thread->quit();
thread->wait();
thread->deleteLater();
myth->deleteLater();
bub->deleteLater();
});
4.8 QThreadQThread提供currentThreadId()方法返回当前的线程ID
qDebug()<<objectName()<<":"<<"getstarted() , tid :"<<QThread::currentThreadId();
QThread 还提供静态的、独立于平台的睡眠函数:sleep()、msleep() 和 usleep()分别允许全秒、毫秒和微秒分辨率。静态函数 currentThreadId() 和 currentThread() 返回当前正在执行的线程的标识符。前者返回线程的平台特定 ID;后者返回一个 QThread 指针。QThread是用来管理线程的,它所依附的线程和它管理的线程并不是同一个东西QThread 所依附的线程,就是执行 QThread t(0) 或QThread * t=new QThread(0) 的线程。QThread 管理的线程,就是 run启动的线程,也就是子线程。如果QThread的对象依附在主线程中,其slot函数会在主线程中执行,而不是子线程。跨不同线程与对象交互时必须小心。作为一般规则,函数只能从创建 QThread 对象本身的线程调用。4.9 线程安全的事件传递可重入和线程安全静态方法QThread::postEvent从线程中传递事件,而不同于事件线程。当使用Qt库互斥量的时候不要做任何阻塞操作。确认你锁定一个递归QMutex的次数和解锁的次数一样,不能多也不能少。在调用除了Qt容器和工具类的任何东西之前锁定Qt应用程序互斥量。确认只在GUI线程中创建的继承和使用了QWidget、QTimer和QSocketNotifier的对象。不要在不是GUI线程的线程中试图调用processEvents()函数。不要把普通的Qt库和支持线程的Qt库混合使用。
所有的GUI类(比如,QWidget和它的子类),操作系统核心类(比如,QProcess)和网络类都不是线程安全的。QRegExp使用一个静态缓存并且也不是线程安全的,即使通过使用QMutex来保护的QRegExp对象。QT通过三种形式提供了对线程的支持。一、平台无关的线程类,二、线程安全的事件投递,三、跨线程的信号-槽连接
Qt 线程类Qt 包含下面一些线程相关的类:QThread 提供了开始一个新线程的方法QThreadStorage 提供逐线程数据存储QMutex 提供相互排斥的锁,或互斥量QMutexLocker 是一个便利类,它可以自动对QMutex加锁与解锁QReadWriteLock 提供了一个可以同时读写操作的锁QReadLocker与QWriteLocker 是便利类,它自动对QReadWriteLock加锁与解锁QSemaphore 提供了一个整型信号量,是互斥量的泛化QWaitCondition 提供了一种方法,使得线程可以在被另外线程唤醒之前一直休眠。
Qt 高级线程类QtConcurrent 开启线程事务QFutureWatcher 观测线程状态QFuture 线程启动类
4.10 Qt 线程同步QMutex, QReadWriteLock, QSemaphore, QWaitCondition 提供了线程同步的手段。使用线程的主要想法是希望它们可以尽可能并发执行,而一些关键点上线程之间需要停止或等待。
QMutexQMutex 提供相互排斥的锁,或互斥量。在一个时刻至多一个线程拥有mutex,假如一个线程试图访问已经被锁定的mutex,那么它将休眠,直到拥有mutex的线程对此mutex解锁。Mutexes常用来保护共享数据访问。
QReadWriterLockQReadWriterLock 与QMutex相似,除了它对 “read”,”write”访问进行区别对待。它使得多个读者可以共时访问数据。使用QReadWriteLock而不是QMutex,可以使得多线程程序更具有并发性。
QSemaphoreQSemaphore 是QMutex的一般化,它可以保护一定数量的相同资源,与此相对,一个mutex只保护一个资源。
QWaitConditionQWaitCondition 允许线程在某些情况发生时唤醒另外的线程。一个或多个线程可以阻塞等待一QWaitCondition ,用wakeOne()或wakeAll()设置一个条件。wakeOne()随机唤醒一个,wakeAll()唤醒所有。
参考资料https://blog.csdn.net/weixin_48424192/article/details/110367584 http://blog.csdn.net/emdfans/article/details/41745007 https://www.cnblogs.com/xyf327/p/15032670.html https://www.cnblogs.com/newstart/archive/2013/06/14/3136022.html
标签:moveToThread,Qt,QObject,线程,多线程,QThread,函数 From: https://www.cnblogs.com/bog-box/p/17897582.html