24. 练习题:第 7 版
24.1. 简介
税费计算应用程序的第7版与第6版完全相同,仅以下细节有所不同:
- Web客户端将同时发送多个HTTP请求。在上一版本中,这些请求是依次发送的。因此,服务器每次只能处理一个请求;
- 服务器将采用多线程架构:能够同时处理多个请求;
- 为追踪这些请求的执行情况,Web 服务器将配备一个日志记录器,用于将请求处理过程中的关键时刻记录到文本文件中;
- 当服务器遇到导致其无法启动的问题时(通常是与 Web 服务器关联的数据库问题),将向应用程序管理员发送电子邮件;
应用程序架构保持不变:

脚本的目录结构如下:

首先通过复制 [http-servers/01] 文件夹来创建 [http-servers/02] 文件夹,随后对其进行修改。
24.2. 实用工具

24.2.1. [Logger] 类
[Logger] 类允许将某些 Web 服务器操作记录到文本文件中:
- 第 10–11 行:我们定义了一个类属性。类属性是该类所有实例共有的属性。使用 [Class.class_attribute] 这种语法来引用它(第 30、39 行)。类属性 [lock] 将作为所有执行第 31–36 行代码的线程的同步对象;
- 第 14–19 行:构造函数接收日志文件的绝对路径。随后打开该文件,并将获取的文件描述符存储在类中;
- 第 17 行:日志文件以“追加”模式 (a) 打开。写入的每一行都将追加到文件末尾;
- 第 22–39 行:[write] 方法允许将作为参数传递的消息写入日志文件。该消息后会附加两项信息:
- 第 24 行:当前日期;
- 第 25 行:当前时间;
- 第 27 行:写入日志的线程名称。这里需要记住的是,Web 应用程序会同时为多个用户提供服务。每个请求都会被分配一个线程来执行。如果该线程被暂停——通常是因为 I/O 操作(网络、文件、数据库)——则处理器控制权将移交给另一个线程。 由于可能发生此类中断,我们无法确保某个线程能在未受干扰的情况下成功向日志文件写入一行内容。因此,来自两个不同线程的日志可能会被混淆。这种风险很低,甚至可能为零,但我们仍决定演示如何同步两个线程对共享资源(本例中为日志文件)的访问;
- 第 30 行:在写入之前,线程请求进入门的钥匙。所请求的钥匙即第 11 行创建的那把。它确实是唯一的:类属性对该类的所有实例而言都是唯一的;
- 在时间点 T1,名为 Thread1 的线程获取了钥匙。随后它可以执行第 33 行;
- 在时间点 T2,Thread1 线程在尚未完成日志写入前就被暂停;
- 在时间点 T3,已获得处理器控制权的线程 Thread2 也必须写入日志。因此它到达第 30 行,请求前门钥匙。系统告知它另一条线程已持有该钥匙。随后它被自动暂停。所有请求此钥匙的线程都会遇到这种情况;
- 在时间点 T4,此前被暂停的线程 Thread1 重新获得处理器控制权,随后完成日志写入;
- 第 32–36 行:向日志文件的写入分为两个步骤:
- 第33行:第17行获取的文件描述符关联了一个缓冲区。第33行的[write]操作将数据写入该缓冲区,而非直接写入文件。随后在满足特定条件时,缓冲区会被刷新到文件中:
- 缓冲区已满;
- 文件描述符被调用 [close] 或 [flush] 操作;
- 第 36 行:我们强制将日志行写入文件。这样做是因为我们希望看到不同线程的日志交错显示。如果不这样做,单个线程的日志将全部在同一时间写入——即第 45 行关闭描述符时。届时将很难判断某些线程是否已被停止:我们必须检查日志中的时间戳;
- 第 39 行:Thread1 线程释放了分配给它的锁。现在该锁可以分配给另一个线程;
- 第 22 行:因此 [write] 方法是同步的:每次只有一个线程向日志文件写入数据。该机制的关键在于第 30 行:无论发生什么情况,只有一个线程获取关键值以继续执行下一行。它会一直持有该关键值,直到将其归还(第 39 行);
- 第 41–45 行:[close] 方法释放了分配给日志文件描述符的资源;
写入日志文件的日志内容将如下所示:
24.2.2. [SendAdminMail] 类
[SendAdminMail] 类允许您在应用程序崩溃时向应用程序管理员发送消息。

[SendAdminMail] 类在 [config] 脚本 [2] 中配置如下:
[SendAdminMail] 类接收第 2 至 13 行中的字典以及电子邮件发送配置。该类定义如下:
- 第 24-54 行:这是示例 |smtp/02| 中已介绍过的代码;
- 第 20 行:我们获取日志记录器的引用。该引用在第 45 行和第 49 行中使用;
24.3. Web 服务器
24.3.1. 配置
该服务器的配置与之前讨论的服务器非常相似。仅 [config.py] 文件有轻微改动:
- 第 40–66 行:我们将与日志记录器相关的元素(第 49 行)以及与向应用程序管理员发送警报邮件相关的元素(第 51–63 行)添加到服务器的配置字典中;
- 第 65 行:为了更直观地观察线程的运行情况,我们将强制部分线程暂停。[sleep_time] 表示暂停时长,单位为秒;
- 第 27–28 行:请注意,我们正在使用上一版 6 中的 [index_controller];
24.3.2. 主脚本 [main]
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | |
- 第 1-10 行:脚本期望有一个 [mysql / pgres] 参数,用于指定要使用的数据库管理系统;
- 第 12–14 行:配置应用程序(Python 路径、layers、数据库);
- 第 16–28 行:应用程序所需的依赖项;
- 第 30-43 行:身份验证管理;
- 第 46–51 行:一个向应用程序管理员发送电子邮件的函数;
- 该函数期望两个参数:
- config:一个包含 [adminMail] 和 [logger] 键的字典;
- 待发送的消息;
- 第 49–50 行:我们准备电子邮件配置;
- 发送电子邮件;
- 第 54–74 行:检查日志文件是否存在;
- 第 70–74 行:如果无法打开日志文件,则向管理员发送电子邮件并退出;
- 第 76–79 行:记录服务器启动信息;
- 第 81–98 行:从数据库中检索税务管理数据;
- 第 88–98 行:若无法检索到这些数据,则在控制台和日志文件中记录错误;
- 第 100–101 行:主线程将不再进行日志记录(创建的线程不会使用相同的文件描述符);
- 第 103–105 行:如果无法连接到数据库,则停止;
- 第 122 行:以多线程模式启动服务器;
[index] 函数(第 114 行)如下:
- 第 4 行:当用户请求 URL / 时执行的函数。由于服务器是多线程的(第 112 行),将创建一个线程来执行该函数。该线程可能在任何时候被中断并暂停,稍后继续执行。当代码访问所有线程共享的资源时,务必牢记这一点。在此情况下,该资源即日志文件:所有线程都会向其写入数据;
- 第 8 行:我们创建了一个日志器的实例。因此,所有线程将拥有不同的日志器实例。不过,这些日志器都指向同一个日志文件。需要注意的是,当一个线程关闭其日志器时,这不会影响其他线程的日志器;
- 第 9–12 行:我们将日志器存储在应用程序的 [config] 字典中,键名取自线程名称。因此,如果有 n 个线程同时运行,[config] 字典中将创建 n 个条目。[config] 是所有线程共享的资源。因此,可能需要进行同步。 我在此做了一个假设。我假设如果两个线程同时在 [config] 文件中创建条目,且其中一个被另一个中断,这不会产生影响。被中断的线程稍后可以完成条目的创建。如果测试表明此假设不成立,则需要对第 12 行的访问进行同步;
- 第 10 行:我们将日志器放入字典中;
- 第 11 行:[threading.current_thread()] 表示正在执行本行的线程,即执行 [index] 函数的线程。我们记录其名称。每个线程都有一个唯一的名称;
- 第 12 行:我们存储该线程的配置。从现在起,我们将始终遵循以下原则:如果存在无法在线程间共享的信息,它仍将放置在通用配置中,但会与该线程的名称相关联;
- 第 14 行:我们记录当前正在执行的请求;
- 第 15–24 行:我们随机暂停某些线程,以便它们将处理器让给另一个线程;
- 第 16 行:我们从配置中获取暂停时长(以秒为单位);
- 第 17 行:只有当暂停时长不为 0 时才会发生暂停;
- 第 19 行:一个取值范围在 [0, 1] 内的随机整数。因此,可能的值只有 0 和 1;
- 第 20 行:仅当随机数为 1 时,线程才会被暂停;
- 第 22 行:记录线程即将被暂停的事实;
- 第 24 行:线程暂停 [sleep_time] 秒;
- 第 26 行:当线程唤醒时,由 [index_controller] 模块执行该请求;
- 第 28–32 行:如果此次执行导致 [500 INTERNAL SERVER ERROR] 错误,则向管理员发送一封电子邮件;
- 第 30–31 行:配置将传递给 [SendAdminMail] 类的 [config_mail] 字典;
- 第 32 行:发送给管理员的消息是结果的 JSON 字符串,该结果将发送给客户端;
- 第 33–34 行:我们记录将发送给客户端的响应(第 36 行);
- 第 37–44 行:处理任何异常;
- 第 39–40 行:如果日志器存在,则记录发生的错误;
- 第 47–48 行:如果日志器存在,则关闭它。最终,该线程在请求开始时创建日志器,并在请求处理完成后关闭它;
24.3.3. 控制器 [index_controller]
执行请求的控制器 [index_controller] 与上一版本相同:

24.3.4. 执行
我们启动 Flask 服务器、邮件服务器 |hMailServer| 以及邮件客户端 |Thunderbird|。我们不启动数据库管理系统 (DBMS)。服务器停止时,控制台输出以下日志:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/02/flask/main.py mysql
[serveur] démarrage du serveur
L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]
Process finished with exit code 2
日志文件 [logs.txt] 内容如下:
2020-07-23 11:51:38.324752, MainThread : [serveur] démarrage du serveur
2020-07-23 11:51:40.355510, MainThread : L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]
2020-07-23 11:51:42.464206, MainThread : [SendAdminMail] Message envoyé à [guest@localhost.com] : [L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]]
使用 Thunderbird 检查管理员的电子邮件 [guest@localhost.com]:

然后启动数据库管理系统并请求该 URL [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000]。日志内容如下:
2020-07-23 11:56:38.891753, MainThread : [serveur] démarrage du serveur
2020-07-23 11:56:38.987999, MainThread : [serveur] connexion à la base de données réussie
2020-07-23 11:56:40.586747, MainThread : [serveur] démarrage du serveur
2020-07-23 11:56:40.655254, MainThread : [serveur] connexion à la base de données réussie
2020-07-23 11:56:54.528360, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=200000' [GET]>
2020-07-23 11:56:54.530653, Thread-2 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
- 第1-4行:请注意服务器启动了两次,因为[Debug=True]模式触发了第二次启动;
- 第 5-6 行:日志让我们了解了一个请求的执行时间,这里是 2.293 毫秒;
24.4. Web 客户端

通过复制 [http-clients/01] 目录来创建 [http-clients/02] 目录。随后我们进行一些修改。
24.4.1. 配置
[http-clients/02] 应用程序的 [config] 配置与 [http-clients/01] 应用程序的配置基本相同,仅有细微差异:
- 第 31-32 行:我们将使用与服务器相同的日志记录器 |Logger|;
- 第 49 行:日志文件的绝对路径;
- 第 60 行:使用 [debug=True] 模式将 Web 服务器的响应写入日志文件;
24.4.2. [dao] 层
[ImpôtsDaoWithHttpClient] 类的代码略有更改:
- 第 17 行:我们存储了通用配置。稍后我们将看到,当 [ImpôtsDaoWithHttpClient] 类的构造函数运行时,[config] 字典中尚未包含第 37 行使用的 [logger] 键。这就是为什么我们无法在构造函数中初始化 [self.__logger](第 23 行);
- 第 21 行:我们在配置中添加了 [debug] 键,用于控制第 33–39 行中的日志记录;
- 第 34 行:如果处于 [debug] 模式;
- 第 36–37 行:对 [self.__logger] 属性的可选初始化。当调用 [calculate_tax] 方法时,[logger] 键已成为 [config] 字典的一部分;
- 第 39 行:我们记录与服务器 HTTP 响应关联的文本文档;
[dao] 层将由多个线程同时执行。不过,此处我们仅创建该层的单个实例(参见 config_layers)。因此,我们必须确保代码中不涉及对共享数据的写入操作,通常指实现 [dao] 层的 [ImpôtsDaoWithHttpClient] 类的属性。然而,在上述代码中,第 37 行修改了类实例的某个属性。 在此情况下,这不会产生任何影响,因为所有线程共享同一个日志器。如果情况并非如此,则必须对第 37 行的访问进行同步。
24.4.3. 主脚本
主脚本 [main] 的演变过程如下:
- 主脚本与之前的客户端脚本不同,它将生成多个执行线程来向服务器发送请求。版本 6 中的客户端是顺序发送所有请求的。只有在收到请求 #[i-1] 的响应后,才会发出请求 #i。在这里,我们想观察服务器在收到多个并发请求时的行为。为此,我们需要使用线程;
- 第 21 行:生成的线程将被放入一个列表中。需要理解的是,[main] 脚本本身也是由一个名为 [MainThread] 的线程执行的。这个主线程将创建其他线程,这些线程负责计算一个或多个纳税人的税款;
- 第 26 行:我们创建一个日志器。该日志器将由所有线程共享;
- 第 32 行:我们获取所有需要计算税款的纳税人;
- 第 39–51 行:我们将这些纳税人分配给多个线程;
- 第 40–41 行:每个线程将处理 1 到 4 名纳税人。该数量由随机数决定;
- [random.randint(1, 4)] 会从 [1, 2, 3, 4] 列表中随机生成一个数字;
- 该线程中的纳税人数量不得超过 [l-i],其中 [l-i] 表示尚未被分配到线程的纳税人数量;
- 因此我们取这两个值中的较小者;
- 第 43 行:一旦知道 [nb_taxpayers](即该线程已处理的纳税人数量),我们就从纳税人列表中取出这些纳税人:
- [slice(10,12)] 表示索引集合 [10, 11, 12];
- [taxpayers[slice(10,12)]] 是列表 [taxpayers[10], taxpayers[11], taxpayers[12]];
- 第 45 行:我们递增 i 的值,该变量控制第 39 行的循环;
- 第 47 行:我们创建一个线程:
- [target=thread_function] 设置线程将要执行的函数。这是第 16–17 行中的函数。它期望三个参数;
- [ags] 是 [thread_function] 函数所期望的三个参数的列表;
创建线程并不意味着立即执行它,它只是创建了一个对象;
- 第 48–49 行:将刚刚创建的线程添加到主线程创建的线程列表中;
- 第 51 行:线程被启动。随后它将与其他活动线程并行运行。在此,它将使用提供的参数执行 [thread_function];
- 第 53–54 行:主线程等待其启动的每个线程完成。让我们举个例子:
- 主线程已启动三个线程 [th1, th2, th3];
- 主线程按 for 循环的顺序等待每个线程(第 53–54 行):[th1, th2, th3];
- 假设线程按 [th2, th1, th3] 的顺序完成;
- 主线程等待 th1 结束。当 th2 结束时,不会发生任何事情;
- 当 th1 结束时,主线程等待 th2。然而,th2 已经结束。主线程随后转到下一个线程并等待 th3;
- 当 th3 结束时,主线程已完成等待并继续执行第 57 行;
- 第 57 行将结果写入结果文件。这是一个关于对象引用的好例子:
- 第 43 行:与线程关联的 [thread_payers] 列表包含 [taxpayers] 列表中对象引用的副本;
- 我们知道,税额计算会修改 [thread_payers] 列表中引用所指向的对象。这些对象将根据税额计算结果进行更新。然而,引用本身并未被修改。因此,初始 [taxpayers] 列表中的引用“看到”或“指向”了已被修改的对象;
线程执行的 [thread_function] 如下:
- 由多个线程同时执行的函数通常难以编写:你必须始终确保代码不会尝试修改线程间共享的数据。当这种情况发生时,你必须对即将被修改的共享数据实现同步访问;
- 第 3 行:该函数接收三个参数:
- [dao]:指向 [dao] 层的引用。该数据是共享的;
- [logger]:对日志器的引用。该数据是共享的;
- [taxpayers]:纳税人列表。该数据不共享:每个线程管理不同的列表;
- 让我们来分析这两个引用 [dao, logger]:
- 我们看到,[dao] 引用所指向的对象有一个 [self.__logger] 引用,该引用会被线程修改,但修改的目的是为所有线程设置一个共同的值;
- [logger] 引用指向一个文件描述符。我们发现向文件写入日志时可能会出现问题。因此,向文件的写入操作已被同步;
- 第 5–6 行:我们记录线程的名称及其必须管理的纳税人数量;
- 第 8–14 行:计算纳税人的税款;
- 第 16 行:记录线程结束;
24.4.4. 执行
按照上一节所述启动 Web 服务器(Web 服务器、DBMS、hMailServer、Thunderbird),然后运行客户端的 [main] 脚本。在文件 [data/output/errors.txt, data/output/results.json] 中,我们得到与上一版本相同的结果。在文件 [data/logs/logs.txt] 中,我们有以下日志:
2020-07-24 10:05:20.942404, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-24 10:05:20.943458, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-24 10:05:20.943458, Thread-2 : début du thread [Thread-2] avec 3 contribuable(s)
2020-07-24 10:05:20.946502, Thread-3 : début du thread [Thread-3] avec 1 contribuable(s)
2020-07-24 10:05:20.946502, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-24 10:05:20.947003, Thread-3 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-24 10:05:20.947003, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-24 10:05:20.950324, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-24 10:05:20.948449, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-24 10:05:20.953645, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-24 10:05:20.976143, Thread-1 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:20.976695, Thread-1 : fin du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-24 10:05:20.976695, Thread-1 : fin du thread [Thread-1]
2020-07-24 10:05:21.973914, Thread-2 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
2020-07-24 10:05:21.973914, Thread-2 : fin du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}
2020-07-24 10:05:21.973914, Thread-2 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-24 10:05:21.977130, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:21.977130, Thread-4 : fin du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}
2020-07-24 10:05:21.977130, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-24 10:05:21.982634, Thread-3 : {"réponse": {"result": {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:21.982634, Thread-5 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:21.983134, Thread-3 : fin du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:21.983134, Thread-5 : fin du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}
2020-07-24 10:05:21.983134, Thread-3 : fin du thread [Thread-3]
2020-07-24 10:05:21.983763, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-24 10:05:22.008562, Thread-5 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:22.008562, Thread-5 : fin du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}
2020-07-24 10:05:22.009062, Thread-5 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-24 10:05:22.016848, Thread-5 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:22.017349, Thread-5 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:22.017349, Thread-5 : fin du thread [Thread-5]
2020-07-24 10:05:23.008486, Thread-2 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
2020-07-24 10:05:23.008486, Thread-2 : fin du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}
2020-07-24 10:05:23.009749, Thread-2 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-24 10:05:23.011722, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:23.013723, Thread-4 : fin du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-24 10:05:23.013723, Thread-4 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-24 10:05:23.024135, Thread-2 : {"réponse": {"result": {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:23.024135, Thread-2 : fin du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:23.025178, Thread-2 : fin du thread [Thread-2]
2020-07-24 10:05:23.025178, Thread-4 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:23.026191, Thread-4 : fin du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:23.026191, Thread-4 : fin du thread [Thread-4]
- 这些日志显示,系统启动了五个线程来计算 11 名纳税人的税款。这五个线程同时向税款计算服务器发送了请求。理解其工作原理非常重要:
- 线程 [Thread-1] 最先启动。当它获得 CPU 资源时,会执行代码直至发送 HTTP 请求。由于必须等待该请求的结果,它会被自动挂起。随后它失去 CPU 资源,由另一个线程接管;
- 第 1–10 行:对于这 5 个线程中的每一个,该过程都会重复。因此,在第 11 行线程 [Thread-1] 收到响应之前,这 5 个线程就已经全部启动了;
- 线程的结束顺序与它们的启动顺序不同。因此,线程 [Thread-3] 最先结束,第 23 行;
在服务器端,文件 [data/logs/logs.txt] 中的日志如下:
2020-07-24 10:05:01.692980, MainThread : [serveur] démarrage du serveur
2020-07-24 10:05:01.877251, MainThread : [serveur] connexion à la base de données réussie
2020-07-24 10:05:03.596162, MainThread : [serveur] démarrage du serveur
2020-07-24 10:05:03.661160, MainThread : [serveur] connexion à la base de données réussie
2020-07-24 10:05:20.968053, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=50000' [GET]>
2020-07-24 10:05:20.969132, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.970316, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=100000' [GET]>
2020-07-24 10:05:20.970316, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.971335, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=55555' [GET]>
2020-07-24 10:05:20.972563, Thread-4 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:20.974796, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=3&salaire=100000' [GET]>
2020-07-24 10:05:20.974796, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.976143, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=30000' [GET]>
2020-07-24 10:05:20.976143, Thread-6 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:21.970615, Thread-2 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}}}
2020-07-24 10:05:21.973914, Thread-3 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:21.977130, Thread-6 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:21.977130, Thread-5 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:22.001693, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=50000' [GET]>
2020-07-24 10:05:22.003013, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:22.003013, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=5&salaire=100000' [GET]>
2020-07-24 10:05:22.003013, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:22.005871, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=0&salaire=200000' [GET]>
2020-07-24 10:05:22.006370, Thread-9 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:22.014170, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=200000' [GET]>
2020-07-24 10:05:22.014170, Thread-10 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:23.003533, Thread-7 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'taux': 0.14, 'décôte': 720, 'réduction': 0}}}
2020-07-24 10:05:23.006434, Thread-8 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:23.018026, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=2&salaire=100000' [GET]>
2020-07-24 10:05:23.019074, Thread-11 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:23.021447, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=0&salaire=100000' [GET]>
2020-07-24 10:05:23.022447, Thread-12 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
- 我们可以看到,11个线程处理了11名纳税人;
- 部分线程被挂起(第6、8、12、14、20、22行),其余则未被挂起(第9、23、25、29、31行);
24.5. [DAO] 层测试
与 |上一版本| 一样,我们正在测试客户端的 [DAO] 层。其原理完全相同:

测试类将在以下环境中执行:

- 配置 [2] 与我们刚刚分析过的配置 [1] 完全相同;
测试类 [TestHttpClientDao] 如下所示:
- 我们为该测试创建一个 |执行配置|;
- 我们启动 Web 服务器及其完整运行环境;
- 运行测试;
结果如下:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-clients/02/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 6.128s
OK
Process finished with exit code 0