Qt 进程和线程之二:启动线程教程

2019-06-24由程序员日记发表于系统教程 浏览28次

目录

Qt提供了对线程的支持,这包括一组与平台无关的线程类、一个线程安全的发送事件的方式,以及跨线程的信号槽的关联。这些使得可以很容易地开发可移植的多线程Qt应用程序,可以充分利用多处理器的机器。多线程编程也可以有效解决在不冻结一个应用程序的用户界面情况下执行一个耗时的操作问题。对应本节的内容,可以在帮助中査看Thread Support in Qt关键字。

这里准备介绍QThread常用函数和启动进程的两种方式:

  • 子类化QThread
  • Worker-Object

一、QThread常用函数

可以将常用的函数按照功能进行以下分类:

  • 线程启动

    • void start()

      调用后会执行run()函数,但在run()函数执行前会发射信号started(),操作系统将根据优先级参数调度线程。如果线程已经在运行,那么这个函数什么也不做。优先级参数的效果取决于操作系统的调度策略。

  • 线程执行

    • int exec()

      每一个线程可以有自己的事件循环,可以通过调用exec()函数来启动事件循环。

    • void run()

      线程的起点,在调用start()之后,新创建的线程就会调用这个函数,默认实现调用exec(),大多数需要重新实现这个函数,便于管理自己的线程。该函数返回后,线程便执行结束,就像应用程序离开main()函数一样。

  • 线程退出

    • void quit()

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

    • void exit(int returnCode = 0)

      使线程退出事件循环,返回0表示成功,任何非0值表示失败。

    • void terminate()

      在极端情况下,可能想要强制终止一个正在执行的线程,这时可以使用terminate()函数。但是,线程可能会立即被终止也可能不会,这取决于操作系统的调度策略,使用terminate()之后再使用QThread::wait(),以确保万无一失。

      警告:使用terminate()函数,线程可能在任何时刻被终止而无法进行一些淸理工作,因此该函数是很危险的,一般不建议使用,只有在绝对必要的时候使用。

    • void requestInterruption()

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

  • 线程等待

    • void msleep(unsigned long msecs) [static]

      强制当前线程睡眠msecs毫秒。

    • void sleep(unsigned long secs) [static]

      强制当前线程睡眠secs秒。

    • void usleep(unsigned long usecs) [static]

      强制当前线程睡眠usecs微秒。

    • bool wait(unsigned long time = ULONG_MAX)

      线程将会被阻塞,等待time毫秒。和sleep不同的是,如果线程退出,wait会返回。

  • 线程状态

    • bool isFinished() const

      判断线程是否结束

    • bool isRunning() const

      判断线程是否正在运行

  • 线程优先级

    • void setPriority(Priority priority)

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

枚举QThread::Priority:

常量值描述QThread::IdlePriority0没有其它线程运行时才调度QThread::LowestPriority1比LowPriority调度频率低QThread::LowPriority2比NormalPriority调度频率低QThread::NormalPriority3操作系统的默认优先级QThread::HighPriority4比NormalPriority调度频繁QThread::HighestPriority5比HighPriority调度频繁QThread::TimeCriticalPriority6尽可能频繁的调度QThread::InheritPriority7使用和创建线程同样的优先级. 这是默认值

二、子类化QThread方式启动线程

Qt中的QThread类提供了平台无关的线程。一个QThread代表了一个在应用程序中可以独立控制的线程,它与进程中的其他线程分享数据,但是是独立执行的。相对于一般的程序都是从main()函数开始执行,QThread从main()函数开始执行。默认的,run()通过调用exec()来开启事件循环。要创建一个线程,需要子类化QThread并且重新实现run()函数。例如:

<pre class="cpp">```
class MyThread : public QThread
{
protected:
    void run();
};

void MyThread::run()
{
    QTcpSocket socket;
    ...
    socket connectToHost(hostName, portNumber);
    exec();
}

这样会在一个线程中创建一个QTcpSocket,然后执行这个线程的事件循环。可以在外部创建该线程的实例,然后调用start()函数来开始执行该线程,start()默认会调用run()函数。当从run()函数返回后,线程便执行结束。

QThread类详解

(3)每一个线程可以有自己的事件循环,可以通过调用exec()函数来启动事件循环, 可以通过调用exit()或者quit()来停止事件循环。在一个线程中拥有一个事件循环, 可以使它能够关联其他线程中的信号到本线程的槽上,这使用了队列关联机制,就是在使用connect()函数进行信号和梢的关联时,将Qt::ConnectionType类型的参数指定为Qt::QueuedConnection。拥有事件循环还可以使该线程能够使用需要事件循环的类,比如QTimer和QTcpSocket类等。注意,在线程中是无法使用任何的部件类的。

(5)静态函数currentThreadld()和currentThread()可以返回当前执行的线程的标识符,前者返回一个该线程的系统特定的ID;后者返回一个QThread指针。QThread也提供了多个平台无关的睡眠函数,其中sleep()精度为秒,msleep()精度为毫秒,usleep()精度为微秒。

实例程序

下面来看一个在图形界面程序中启动一个线程的例子,在界面上有两个按钮,一个用于开启一个线程,一个用于关闭该线程。新建Qt Gui应用,名称为myThread,类名为Dialog,基类选择QDialog。完成后进入设计模式,向界面中放人两个Push Button按钮,将第一个按钮的显示文本更改为“启动线程”,名称更改为startButton;将第二个按钮的显示文本更改为 “终止线程”,名称更改为stopButton,将其enabled属性取消选中。然后向项目中添加新的C++类,类名设置为“MyThread”,基类设置为“QThread”,类型信息选择“继承自QObject”。完成后进入mythread.h文件,修改如下:

<pre class="cpp">```
#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QThread>

class MyThread : public QThread
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = 0);
    void stop();

protected:
    void run();

private:
    volatile bool stopped;
};

#endif // MYTHREAD_H

这里stopped变量使用了volatile关键字,这样可以使它在任何时候都保持最新的值,从而可以避免在多个线程中访问它时出错。然后进入mythread.cpp文件中,修改如下:

<pre class="cpp">```
#include "mythread.h"
#include <QDebug>

MyThread::MyThread(QObject *parent) :
    QThread(parent)
{
    stopped = false;
}

void MyThread::run()
{
    qreal i = 0;
    while (!stopped) 
    {
        qDebug() << QString("in MyThread: %1").arg(i);
        msleep(1000);
        i++;
    }
    stopped = false;
}

void MyThread::stop()
{
    stopped = true;
}

(1)构造函数中将stopped变量初始化为false。

(2)run()函数中一直判断stopped变量的值,只要它为false,那么每过1秒就一直打印i值递增的字符串。

(3)top()函数中将stopped变量设置为了true,这样便可以结束run()函数中的循环,从而从run()函数中退出,这样整个线程也就结束了。这里使用了stopped变量来实现了进程的终止,并没有使用危险的terminate()函数。

下面在Dialog类中使用自定义的线程。先到dialog.h文件中,添加头文件"mythread.h",然后添加私有对象:

<pre class="cpp">```
#include "mythread.h"
...
private:    
    MyThread thread;

下面到设计模式,分别进入两个按钮的单击信号对应的槽,更改如下:

<pre class="cpp">```
// 启动线程按钮
void Dialog::on_startButton_clicked()
{
    thread.start();
    ui->startButton->setEnabled(false);
    ui->stopButton->setEnabled(true);
}

// 终止线程按钮
void Dialog::on_stopButton_clicked()
{
    if (thread.isRunning())
    {
        thread.stop();
        ui->startButton->setEnabled(true);
        ui->stopButton->setEnabled(false);
    }
}

启动线程时调用了start()函数,然后设置了两个按钮的状态。在终止线程时, 先使用isRunning()来判断线程是否在运行,如果是,则调用stop()函数来终止线程,并且更改两个按钮的状态。现在运行程序,单击“启动线程”按钮,査看应用程序输出栏的输出,然后再按下“终止线程”按钮,可以看到已经停止输出了。

三、worker-object方式启动线程

还有一种创建线程的方法,就是使用worker-object通过QObject::moveToThread将它们移动到线程中。例如:

<pre class="cpp">```
class Worker : public QObject
{
    Q_OBJECT

public slots:
    void doWork(const QString &parameter) 
    {
        QString result;
        // 这里是昂贵的或阻塞的操作
        emit resultReady(result);
    }

signals:
    void resultReady(const QString &result);
};

class Controller : public QObject
{
    Q_OBJECT
    QThread workerThread;
    
public:
    Controller() 
    {
        Worker *worker = new 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);
        workerThread.start();
    }
    
    ~Controller() 
    {
        workerThread.quit();
        workerThread.wait();
    }
    
public slots:
    void handleResults(const QString &);
    
signals:
    void operate(const QString &);
};

这样Worker槽中的代码将在一个单独的线程中执行,使用这种方式可以很容易地将一些费时的操作放到单独的工作线程中来完成。可以将任意线程中任意对象的任意一个信号关联到Worker的槽上,不同线程间的信号和槽进行关联是安全的。

参考:

Qt 之 QThread