Qt 学习之路 2(75):线程总结

时间:2021-08-04 22:54:32

前面我们已经详细介绍过有关线程的一些值得注意的事项。现在我们开始对线程做一些总结。

有关线程,你可以做的是:

  • QThread子类添加信号。这是绝对安全的,并且也是正确的(前面我们已经详细介绍过,发送者的线程依附性没有关系)

不应该做的是:

  • 调用moveToThread(this)函数
  • 指定连接类型:这通常意味着你正在做错误的事情,比如将QThread控制接口与业务逻辑混杂在了一起(而这应该放在该线程的一个独立对象中)
  • QThread子类添加槽函数:这意味着它们将在错误的线程被调用,也就是QThread对象所在线程,而不是QThread对象管理的线程。这又需要你指定连接类型或者调用moveToThread(this)函数
  • 使用QThread::terminate()函数

不能做的是:

  • 在线程还在运行时退出程序。使用QThread::wait()函数等待线程结束
  • QThread对象所管理的线程仍在运行时就销毁该对象。如果你需要某种“自行销毁”的操作,你可以把finished()信号同deleteLater()槽连接起来

那么,下面一个问题是:我什么时候应该使用线程?

首先,当你不得不使用同步 API 的时候。

如果你需要使用一个没有非阻塞 API 的库或代码(所谓非阻塞 API,很大程度上就是指信号槽、事件、回调等),那么,避免事件循环被阻塞的解决方案就是使用进程或者线程。不过,由于开启一个新的工作进程,让这个进程去完成任务,然后再与当前进程进行通信,这一系列操作的代价都要比开启线程要昂贵得多,所以,线程通常是最好的选择。

一个很好的例子是地址解析服务。注意我们这里并不讨论任何第三方 API,仅仅假设一个有这样功能的库。这个库的工作是将一个主机名转换成地址。这个过程需要去到一个系统(也就是域名系统,Domain Name System, DNS)执行查询,这个系统通常是一个远程系统。一般这种响应应该瞬间完成,但是并不排除远程服务器失败、某些包可能会丢失、网络可能失去链接等等。简单来说,我们的查询可能会等几十秒钟。

UNIX 系统上的标准 API 是阻塞的(不仅是旧的gethostbyname(3),就连新的getservbyname(3)getaddrinfo(3)也是一样)。Qt 提供的QHostInfo类同样用于地址解析,默认情况下,内部使用一个QThreadPool提供后台运行方式的查询(如果关闭了 Qt 的线程支持,则提供阻塞式 API)。

另外一个例子是图像加载和缩放。QImageReaderQImage只提供了阻塞式 API,允许我们从设备读取图片,或者是缩放到不同的分辨率。如果你需要处理很大的图像,这种任务会花费几十秒钟。

其次,当你希望扩展到多核应用的时候。

线程允许你的程序利用多核系统的优势。每一个线程都可以被操作系统独立调度,如果你的程序运行在多核机器上,调度器很可能会将每一个线程分配到各自的处理器上面运行。

举个例子,一个程序需要为很多图像生成缩略图。一个具有固定 n 个线程的线程池,每一个线程交给系统中的一个可用的 CPU 进行处理(我们可以使用QThread::idealThreadCount()获取可用的 CPU 数)。这样的调度将会把图像缩放工作交给所有线程执行,从而有效地提升效率,几乎达到与 CPU 数的线性提升(实际情况不会这么简单,因为有时候 CPU 并不是瓶颈所在)。

第三,当你不想被别人阻塞的时候。

这是一个相当高级的话题,所以你现在可以暂时不看这段。这个问题的一个很好的例子是在 WebKit 中使用QNetworkAccessManager。WebKit 是一个现代的浏览器引擎。它帮助我们展示网页。Qt 中的QWebView就是使用的 WebKit。

QNetworkAccessManager则是 Qt 处理 HTTP 请求和响应的通用类。我们可以将它看做浏览器的网络引擎。在 Qt 4.8 之前,这个类没有使用任何协助工作线程,所有的网络处理都是在QNetworkAccessManager及其QNetworkReply所在线程完成。

虽然在网络处理中不使用线程是一个好主意,但它也有一个很大的缺点:如果你不能及时从 socket 读取数据,内核缓冲区将会被填满,于是开始丢包,传输速度将会直线下降。

socket 活动(也就是从一个 socket 读取一些可用的数据)是由 Qt 的事件循环管理的。因此,阻塞事件循环将会导致传输性能的损失,因为没有人会获得有数据可读的通知,因此也就没有人能够读取这些数据。

但是什么会阻塞事件循环?最坏的答案是:WebKit 自己!只要收到数据,WebKit 就开始生成网页布局。不幸的是,这个布局的过程非常复杂和耗时,因此它会阻塞事件循环。尽管阻塞时间很短,但是足以影响到正常的数据传输(宽带连接在这里发挥了作用,在很短时间内就可以塞满内核缓冲区)。

总结一下上面所说的内容:

  • WebKit 发起一次请求
  • 从服务器响应获取一些数据
  • WebKit 利用到达的数据开始进行网页布局,阻塞事件循环
  • 由于事件循环被阻塞,也就没有了可用的事件循环,于是操作系统接收了到达的数据,但是却不能从QNetworkAccessManager的 socket 读取
  • 内核缓冲区被填满,传输速度变慢

网页的整体加载时间被自身的传输速度的降低而变得越来越坏。

注意,由于QNetworkAccessManagerQNetworkReply都是QObject,所以它们都不是线程安全的,因此你不能将它们移动到另外的线程继续使用。因为它们可能同时有两个线程访问:你自己的和它们所在的线程,这是因为派发给它们的事件会由后面一个线程的事件循环发出,但你不能确定哪一线程是“后面一个”。

Qt 4.8 之后,QNetworkAccessManager默认会在一个独立的线程处理 HTTP 请求,所以导致 GUI 失去响应以及操作系统缓冲区过快填满的问题应该已经被解决了。

那么,什么情况下不应该使用线程呢?

定时器

这可能是最容易误用线程的情况了。如果我们需要每隔一段时间调用一个函数,很多人可能会这么写代码:

 
 
 
 
 

C++

 
1
2
3
4
5
// 最错误的代码
while (condition) {
    doWork();
    sleep(1); // C 库里面的 sleep(3) 函数
}

当读过我们前面的文章之后,可能又会引入线程,改成这样的代码:

 
 
 
 
 

C++

 
1
2
3
4
5
6
7
8
9
10
11
// 错误的代码
class Thread : public QThread {
protected:
    void run() {
        while (condition) {
            // 注意,如果我们要在别的线程修改 condition,那么它也需要加锁
            doWork();
            sleep(1); // 这次是 QThread::sleep()
        }
    }
};

最好最简单的实现是使用定时器,比如QTimer,设置 1s 超时,然后将doWork()作为槽:

 
 
 
 
 

C++

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Worker : public QObject
{
Q_OBJECT
public:
    Worker()
    {
        connect(&timer, SIGNAL(timeout()), this, SLOT(doWork()));
        timer.start(1000);
    }
private slots:
    void doWork()
    {
        /* ... */
    }
private:
    QTimer timer;
};

我们所需要的就是开始事件循环,然后每隔一秒doWork()就会被自动调用。

网络/状态机

下面是一个很常见的处理网络操作的设计模式:

 
 
 
 
 

C++

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
socket->connect(host);
socket->waitForConnected();
 
data = getData();
socket->write(data);
socket->waitForBytesWritten();
 
socket->waitForReadyRead();
socket->read(response);
 
reply = process(response);
 
socket->write(reply);
socket->waitForBytesWritten();
/* ... */

在经过前面几章的介绍之后,不用多说,我们就会发现这里的问题:大量的waitFor*()函数会阻塞事件循环,冻结 UI 界面等等。注意,上面的代码还没有加入异常处理,否则的话肯定会更复杂。这段代码的错误在于,我们的网络实际是异步的,如果我们非得按照同步方式处理,就像拿起枪打自己的脚。为了解决这个问题,很多人会简单地将这段代码移动到一个新的线程。

一个更抽象的例子是:

 
 
 
 
 

C++

 
1
2
3
4
5
6
7
8
9
10
11
12
result = process_one_thing();
 
if (result->something()) {
    process_this();
} else {
    process_that();
}
 
wait_for_user_input();
input = read_user_input();
process_user_input(input);
/* ... */

这段抽象的代码与前面网络的例子有“异曲同工之妙”。

让我们回过头来看看这段代码究竟是做了什么:我们实际是想创建一个状态机,这个状态机要根据用户的输入作出合理的响应。例如我们网络的例子,我们实际是想要构建这样的东西:

 
 
 
 
 

C++

 
1
2
3
4
5
空闲 → 正在连接(调用<code>connectToHost()</code>)
正在连接 → 成功连接(发出<code>connected()</code>信号)
成功连接 → 发送登录数据(将登录数据发送到服务器)
发送登录数据 → 登录成功(服务器返回 ACK)
发送登录数据 → 登录失败(服务器返回 NACK)

以此类推。

既然知道我们的实际目的,我们就可以修改代码来创建一个真正的状态机(Qt 甚至提供了一个状态机类:QStateMachine)。创建状态机最简单的方法是使用一个枚举来记住当前状态。我们可以编写如下代码:

 
 
 
 
 

C++

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Object : public QObject
{
    Q_OBJECT
    enum State {
        State1, State2, State3 /* ... */
    };
    State state;
public:
    Object() : state(State1)
    {
        connect(source, SIGNAL(ready()), this, SLOT(doWork()));
    }
private slots:
    void doWork() {
        switch (state) {
        case State1:
            /* ... */
            state = State2;
            break;
        case State2:
            /* ... */
            state = State3;
            break;
        /* ... */
        }
    }
};

source对象是哪来的?这个对象其实就是我们关心的对象:例如,在网络的例子中,我们可能希望把 socket 的QAbstractSocket::connected()或者QIODevice::readyRead()信号与我们的槽函数连接起来。当然,我们很容易添加更多更合适的代码(比如错误处理,使用QAbstractSocket::error()信号就可以了)。这种代码是真正异步、信号驱动的设计。

将任务分割成若*分

假设我们有一个很耗时的计算,我们不能简单地将它移动到另外的线程(或者是我们根本无法移动它,比如这个任务必须在 GUI 线程完成)。如果我们将这个计算任务分割成小块,那么我们就可以及时返回事件循环,从而让事件循环继续派发事件,调用处理下一个小块的函数。回一下如何实现队列连接,我们就可以轻松完成这个任务:将事件提交到接收对象所在线程的事件循环;当事件发出时,响应函数就会被调用。

我们可以使用QMetaObject::invokeMethod()函数,通过指定Qt::QueuedConnection作为调用类型来达到相同的效果。不过这要求函数必须是内省的,也就是说这个函数要么是一个槽函数,要么标记有Q_INVOKABLE宏。如果我们还需要传递参数,我们需要使用qRegisterMetaType()函数将参数注册到 Qt 元类型系统。下面是代码示例:

 
 
 
 
 

C++

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Worker : public QObject
{
    Q_OBJECT
public slots:
    void startProcessing()
    {
        processItem(0);
    }
 
    void processItem(int index)
    {
        /* 处理 items[index] ... */
        if (index < numberOfItems) {
            QMetaObject::invokeMethod(this,
                                      "processItem",
                                      Qt::QueuedConnection,
                                      Q_ARG(int, index + 1));
        }
    }
};

由于没有任何线程调用,所以我们可以轻易对这种计算任务执行暂停/恢复/取消,以及获取结果。

至此,我们利用五个章节将有关线程的问题简单介绍了下。线程应该说是全部设计里面最复杂的部分之一,所以这部分内容也会比较困难。在实际运用中肯定会更多的问题,这就只能让我们具体分析了。

http://www.devbean.net/2013/12/qt-study-road-2-thread-summary/

Qt 学习之路 2(75):线程总结的更多相关文章

  1. Qt 学习之路 2(74):线程和 QObject

    Home / Qt 学习之路 2 / Qt 学习之路 2(74):线程和 QObject Qt 学习之路 2(74):线程和 QObject  豆子  2013年12月3日  Qt 学习之路 2  2 ...

  2. Qt 学习之路 2(73):Qt 线程相关类

    Home / Qt 学习之路 2 / Qt 学习之路 2(73):Qt 线程相关类 Qt 学习之路 2(73):Qt 线程相关类  豆子  2013年11月26日  Qt 学习之路 2  7条评论 希 ...

  3. Qt 学习之路 2(72):线程和事件循环

    Qt 学习之路 2(72):线程和事件循环 <理解不清晰,不透彻>  --  有需求的话还需要进行专题学习  豆子  2013年11月24日  Qt 学习之路 2  34条评论 前面一章我 ...

  4. Qt 学习之路 2(71):线程简介

    Qt 学习之路 2(71):线程简介 豆子 2013年11月18日 Qt 学习之路 2 30条评论 前面我们讨论了有关进程以及进程间通讯的相关问题,现在我们开始讨论线程.事实上,现代的程序中,使用线程 ...

  5. Qt 学习之路 2(70):进程间通信

    Qt 学习之路 2(70):进程间通信 豆子 2013年11月12日 Qt 学习之路 2 16条评论 上一章我们了解了有关进程的基本知识.我们将进程理解为相互独立的正在运行的程序.由于二者是相互独立的 ...

  6. Qt 学习之路 2(69):进程

    Qt 学习之路 2(69):进程 豆子 2013年11月9日 Qt 学习之路 2 15条评论 进程是操作系统的基础之一.一个进程可以认为是一个正在执行的程序.我们可以把进程当做计算机运行时的一个基础单 ...

  7. Qt 学习之路 2(68):访问网络(4)

    Home / Qt 学习之路 2 / Qt 学习之路 2(68):访问网络(4) Qt 学习之路 2(68):访问网络(4) 豆子 2013年11月7日 Qt 学习之路 2 19条评论 前面几章我们了 ...

  8. Qt 学习之路 2(66):访问网络(2)

    Home / Qt 学习之路 2 / Qt 学习之路 2(66):访问网络(2) Qt 学习之路 2(66):访问网络(2)  豆子  2013年10月31日  Qt 学习之路 2  27条评论 上一 ...

  9. Qt 学习之路 2(65):访问网络(1)

    Home / Qt 学习之路 2 / Qt 学习之路 2(65):访问网络(1) Qt 学习之路 2(65):访问网络(1)  豆子  2013年10月11日  Qt 学习之路 2  18条评论 现在 ...

  10. Qt 学习之路 2(51):布尔表达式树模型

    Qt 学习之路 2(51):布尔表达式树模型 豆子 2013年5月15日 Qt 学习之路 2 17条评论 本章将会是自定义模型的最后一部分.原本打算结束这部分内容,不过实在不忍心放弃这个示例.来自于 ...

随机推荐

  1. asp&period;net两种方式的短信接口使用(提供接口的都是收费的)

    一种是http请求的方式,另一种就是提供WebService接口供调用的. //服务商 sms.webchinese.cn //sms_url="http://sms.webchinese. ...

  2. 用fxc&period;exe编译shader文件(&ast;&period;fx&comma; &ast;&period;hlsl)的设置

    原文出自:http://msdn.microsoft.com/en-us/library/windows/desktop/bb509709(v=vs.85).aspx#Profiles 拿DX11 S ...

  3. Leetcode&colon; Number of Boomerangs

    Given n points in the plane that are all pairwise distinct, a "boomerang" is a tuple of po ...

  4. 高动态范围光照(High Dynamic Range Imaging,简称HDRI或HDR)

    1  HDR基本概念 高动态范围光照(High Dynamic Range Imaging,简称HDRI或HDR),是一种表达超过了显示器所能表现的亮度范围的图像映射技术,已成为目前游戏应用不可或缺的 ...

  5. SQL&ast;Net more data from client

    SQL*Net more data from client The server is waiting on the client to send more data to its client sh ...

  6. Amazon API Gateway Importer整合过程小结

    (1)需要将swagger json转换成amazon api gateway 所需要的格式(根据Method Request中 Request PathsURL Query String Param ...

  7. plugin

    http://wicky.nillia.ms/headroom.js/ http://makotot.github.io/sidebar/ http://www.nodebeginner.org/in ...

  8. FreeRTOS——错误排查

    1. printf-stdarg.c 在调用标准库函数时,栈空间的用量可能急剧上升,特别是IO与字符串处理函数,比如 sprintf(). 在FreeRTOS下载包中有一个名为printf-stdar ...

  9. ssl证书专题&lpar;2&rpar;&colon;自签名ssl 证书生成

    参考: https://www.cnblogs.com/littleatp/p/5878763.html https://www.cnblogs.com/hnxxcxg/p/7610582.html

  10. RTP协议全解析(H264码流和PS流)

    转自:http://blog.csdn.net/chen495810242/article/details/39207305 写在前面:RTP的解析,网上找了很多资料,但是都不全,所以我力图整理出一个 ...