1 线程的基本概念
通俗地来说,线程是进程中实际执行代码的最小单元,它由操作系统安排调度(何时启动、何时运行和暂停以及何时消亡)。在一个进程中,线程是实际干活的单位。因此一个进程至少得有一个线程,我们把这个线程称之为“主线程”。
在Qt中,如果管理线程的线程对象被销毁时该线程仍在运行,则程序将会报告异常。所以在Qt程序中,如果退出主线程时仍有子线程在运行,程序将会报告异常。除非管理这些子线程的对象在程序退出时不会被销毁。例如:
// 正常退出的程序
int main(int argc, char *argv[])
{
SubThread *subThread = new SubThread;
subThread->start();
return 0;
}
// 退出异常的程序
int main(int argc, char *argv[])
{
SubThread subThread;
subThread.start();
return 0;
}
退出异常提示如下图所示:
第一段程序虽然正常退出了,但是发生了内存泄漏。第二段程序则是直接在退出时发生了异常。所以在使用多线程时,一定要确保线程的正常退出。
2 线程的创建、启动和退出
在Qt中,线程的创建和使用是依赖于QThread这个类。QThread提供了一种依赖于操作系统的线程管理方式。在程序中,每个QThread对象只管理一个线程,而每个线程都是从QThread类提供的run()函数开始执行。
2.1 QThread类创建线程的两种做法
2.1.1 继承QThread。
通过继承QThread类来重写QThread的run()函数。从而在run()函数中实现我们需要完成的功能。(QThread自身的run()函数是protected且virtual的)。如:
class SubThread : public QThread
{
Q_OBJECT
public:
explicit SubThread(QObject *parent = nullptr);
signals:
// QThread interface
protected:
void run() Q_DECL_OVERRIDE;
};
void SubThread::run()
{
qDebug() << "sub thread start";
qDebug() << "sub thread id=" << QThread::currentThreadId();
int i = 0;
while (i <= 1000)
{
if (i % 100 == 0)
{
qDebug() << i << " *** sub thread is running....";
}
i++;
}
qDebug() << "sub thread exit";
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qDebug() << "main thread id = " << QThread::currentThreadId();
SubThread sub;
sub.start();
return a.exec();
}
上述程序的运行结果为:
2.1.2 使用moveToThread()方法将一个QObject对象移动到一个QThread对象中
这种方法用到两个对象。一个是实现功能的worker对象,一个是提供线程能力的QThread对象。步骤如下:
-
第一步:创建worker。如:
// 实现功能的worker class ThreadWorker : public QObject { Q_OBJECT public: explicit ThreadWorker(QObject *parent = nullptr); void doWork(); // 作为线程处理函数 void exit(); private: bool m_exit; }; void ThreadWorker::doWork() { qDebug() << "worker start"; qDebug() << "sub thread id=" << QThread::currentThreadId(); m_exit = false; int i = 0; while (!m_exit && i <= 1000) { if (i % 100 == 0) { qDebug() << i << " *** worker is running...."; } i++; } qDebug() << "worker exit"; } void ThreadWorker::exit() { m_exit = true; }
-
第二步:将worker移入另外的线程管理对象中。
QThread *subThread = new QThread(this); m_worker = new ThreadWorker; m_worker->moveToThread(subThread);
-
第三步:启动线程。这里启动线程,不仅要调用QThread的start()方法,还需要调用worker的线程处理函数。上例中即是doWork()函数。对于start()调用,则是在调用线程中直接执行subThread.start()即可,而对于doWork()的调用,则是必须通过信号与槽方式来执行。这是为了确保对doWork()的调用是在不同的线程中。这里分为两种情形:
(1). 先调用了start(),然后根据用户操作和程序执行情况触发子线程处理信号,然后在该信号对应的槽中调用doWork()。如:Widget::Widget(QWidget *parent) : QWidget(parent) { qDebug() << QThread::currentThreadId(); QThread *subThread = new QThread(this); m_worker = new ThreadWorker; m_worker->moveToThread(subThread); QPushButton *startBtn = new QPushButton(this); startBtn->setText("start"); startBtn->setGeometry(0,0, 100, 50); connect(startBtn, &QPushButton::clicked, this, &Widget::startWorker); connect(this, &Widget::workerStart, m_worker, &ThreadWorker::doWork); subThread->start(); } void Widget::startWorker() { emit workerStart(); }
这里,doWork()是作为workerStart信号的槽被调用的。this是当前线程的对象,而m_worker是subThread中的对象。这两个对象处于不同的线程中,所以这个connect的连接方式是Qt::QueuedConnection。而对于Qt::QueuedConnection连接方式,槽函数是执行在接收信号的对象所在的线程中的。所以,doWork()是执行在其他线程中。
(2). 在用户操作和程序执行情况触发子线程处理信号时,调用start(),然后在QThread的started信号的槽函数中调用doWork()。如:Widget::Widget(QWidget *parent) : QWidget(parent) { qDebug() << QThread::currentThreadId(); m_thread = new QThread(this); m_worker = new ThreadWorker; m_worker->moveToThread(m_thread); QPushButton *startBtn = new QPushButton(this); startBtn->setText("start"); startBtn->setGeometry(0,0, 100, 50); connect(startBtn, &QPushButton::clicked, this, &Widget::startWorker); QObject::connect(m_thread, &QThread::started, [=](){ qDebug() << QThread::currentThreadId(); m_worker->doWork(); }); } void Widget::startWorker() { m_thread->start(); }
这里是通过匿名槽函数来调用doWork()的。而对于槽函数是匿名函数的connect,其连接方式为Qt::DirectConnection,其槽函数执行在发出信号的线程中。所以,这里的doWork()也是执行在其他线程中。
针对上面两种情形,(1)中在线程处理函数被调用之前就启动了线程,这可能会导致资源上的一些浪费;而(2)中在每次操作时去启动线程,则在每次启动时必须先检查线程的状态,这无疑会增加逻辑控制的复杂度。
在上例中moveToThread()的参数除了是QThread对象外,也可以是QThread的子类对象。如上面的SubThread的对象。如果使用的是QThread的子类对象,则需要注意的是QThread的run()方法中默认包含一个event loop。所以,使用的子类对象的run()中也需要提供一个event loop。否则线程会提前退出从而导致doWork()函数无法执行。在上面所述的(1)情形中,由于先调用的start(),所以run()会优先执行,如果没有event loop,则在run()执行完成后线程就将退出了,所以在触发doWork时,其将不会执行。而在(2)情形中,doWork()会优先于run()执行,所以对于(2)中的调用,即使run()中没有event loop,doWork()也会执行一次。但是如果doWork中没有阻塞处理,doWork就将只能运行一次,因为doWork已经返回,这时再启动线程也就毫无意义了。如果doWork中有阻塞,则线程的start()函数可能都无法北调用,因为程序发生了阻塞,所以这也毫无意义。因此,线程的start()一定要早于doWork被调用。
2.2 线程退出
正如前面所述,如果线程未正常退出,则在销毁线程管理对象时将会发生异常。而要使线程退出,一种方法是使QThread的run()函数正常返回;另一种方法是强制终止线程。QThread类提供了terminate()函数来强制终止一个线程。不过该方法的最终执行依赖于系统的时间调度,所以其行为是不可控的。而且使用此函数时,线程不能清理其之前创建的一些资源,所以并不推荐使用这种方法。通常是使用QThread提供的quit()和wait()函数来使一个线程退出。
- quit():quit是使run()函数中的event loop退出。比如,run()中含有一个名为threadLoop的event loop,则quit的作用相当于threadLoop.exit()。所以,如果run()函数中没有event loop,则quit()函数并不起什么作用;而如果run()函数含有其他循环处理,在要是run()函数返回,则必须先要退出这个循环处理。如上面SubThread中的run()函数,其中并没有event loop,所以要使其退出,只能等待其执行结束。如果循环处理时间比较长,则可以通过设置标识来控制循环退出。上面ThreadWorker中的m_exit,这就是一个控制循环退出的标识。通过ThreadWorker使用线程的退出示例如下:
void Widget::stopThread() { m_worker->exit(); // 使线程处理函数返回 m_thread->quit(); // 使QThread的run()函数返回 m_thread->wait(1000); // 等待线程处理完成 }
- wait():使程序阻塞指定的时间。由于quit()函数是非阻塞的,而线程清理一些资源是需要花费一些时间的。所以通常会在调用quit()后调用wait()来使程序等待一段时间。但是这不能作为线程结束的可靠依据。线程结束还是需要通过QThread的finished信号来处理。
3 线程同步(安全)
当一个变量或对象需要在不同的线程之间被使用时,则就可能存在同步问题。如:
void SubThread::run()
{
qDebug() << "sub thread start";
qDebug() << "sub thread id=" << QThread::currentThreadId();
m_exit = false;
int i = 0;
while (!m_exit && i <= 100000)
{
if (i % 100 == 0)