24. Übungsaufgabe: Version 7
24.1. Einführung
Version 7 der Steuerberechnungsanwendung ist bis auf die folgenden Details identisch mit Version 6:
- Der Web-Client sendet mehrere HTTP-Anfragen gleichzeitig. In der vorherigen Version wurden diese Anfragen nacheinander gesendet. Der Server konnte daher jeweils nur eine einzige Anfrage bearbeiten;
- Der Server wird multithreaded sein: Er wird in der Lage sein, mehrere Anfragen gleichzeitig zu verarbeiten;
- Um die Ausführung dieser Anfragen zu verfolgen, wird der Webserver mit einem Logger ausgestattet, der wichtige Momente der Anfrageverarbeitung in einer Textdatei aufzeichnet;
- Der Server sendet eine E-Mail an den Anwendungsadministrator, wenn er auf ein Problem stößt, das den Start verhindert, typischerweise ein Problem mit der dem Webserver zugeordneten Datenbank;
Die Anwendungsarchitektur bleibt unverändert:

Die Verzeichnisstruktur der Skripte sieht wie folgt aus:

Der Ordner [http-servers/02] wird zunächst durch Kopieren des Ordners [http-servers/01] erstellt. Anschließend werden Änderungen daran vorgenommen.
24.2. Die Hilfsprogramme

24.2.1. Die Klasse [Logger]
Die Klasse [Logger] ermöglicht es, bestimmte Aktionen des Webservers in einer Textdatei zu protokollieren:
- Zeilen 10–11: Wir definieren ein Klassenattribut. Ein Klassenattribut ist eine Eigenschaft, die allen Instanzen der Klasse gemeinsam ist. Es wird mit der Notation [Klasse.Klassenattribut] referenziert (Zeilen 30, 39). Das Klassenattribut [lock] dient als Synchronisationsobjekt für alle Threads, die den Code in den Zeilen 31–36 ausführen;
- Zeilen 14–19: Der Konstruktor erhält den absoluten Pfad der Protokolldatei. Diese Datei wird dann geöffnet, und der abgerufene Dateideskriptor wird in der Klasse gespeichert;
- Zeile 17: Die Protokolldatei wird im Modus „Anhängen“ (a) geöffnet. Jede geschriebene Zeile wird an das Ende der Datei angehängt;
- Zeilen 22–39: Die Methode [write] ermöglicht es, eine als Parameter übergebene Nachricht in die Protokolldatei zu schreiben. Dieser Nachricht werden zwei Informationen angehängt:
- Zeile 24: das aktuelle Datum;
- Zeile 25: die aktuelle Uhrzeit;
- Zeile 27: der Name des Threads, der das Protokoll schreibt. Es ist wichtig, sich hier vor Augen zu halten, dass eine Webanwendung mehrere Benutzer gleichzeitig bedient. Jeder Anfrage wird ein Thread zugewiesen, um sie auszuführen. Wenn dieser Thread angehalten wird – typischerweise für eine E/A-Operation (Netzwerk, Dateien, Datenbank) – wird der Prozessor an einen anderen Thread übergeben. Aufgrund dieser möglichen Unterbrechungen können wir nicht sicher sein, dass es einem Thread gelingt, eine Zeile in die Protokolldatei zu schreiben, ohne unterbrochen zu werden. Es besteht daher das Risiko, dass Protokolle von zwei verschiedenen Threads durcheinander geraten. Das Risiko ist gering, vielleicht sogar gleich Null, aber wir haben uns dennoch entschlossen, zu zeigen, wie der Zugriff zweier Threads auf eine gemeinsam genutzte Ressource, in diesem Fall die Protokolldatei, synchronisiert wird;
- Zeile 30: Vor dem Schreiben fordert der Thread den Schlüssel für die Eingangstür an. Der angeforderte Schlüssel ist derjenige, der in Zeile 11 erstellt wurde. Er ist tatsächlich eindeutig: Ein Klassenattribut ist für alle Instanzen der Klasse eindeutig;
- Zum Zeitpunkt T1 erhält ein Thread namens Thread1 den Schlüssel. Er kann dann Zeile 33 ausführen;
- Zum Zeitpunkt T2 wird der Thread Thread1 angehalten, noch bevor er das Schreiben des Protokolls beendet hat;
- Zum Zeitpunkt T3 muss der Thread Thread2, der den Prozessor übernommen hat, ebenfalls ein Protokoll schreiben. Er erreicht somit Zeile 30, wo er den Schlüssel für die Eingangstür anfordert. Ihm wird mitgeteilt, dass ein anderer Thread diesen bereits besitzt. Er wird daraufhin automatisch angehalten. Dies gilt für alle Threads, die diesen Schlüssel anfordern;
- Zum Zeitpunkt T4 erhält der Thread Thread1, der angehalten worden war, den Prozessor zurück. Er beendet daraufhin das Schreiben des Protokolls;
- Zeilen 32–36: Das Schreiben in die Protokolldatei erfolgt in zwei Schritten:
- Zeile 33: Der in Zeile 17 erhaltene Dateideskriptor arbeitet mit einem Puffer. Der [write]-Befehl in Zeile 33 schreibt in diesen Puffer, jedoch nicht direkt in die Datei. Der Puffer wird dann unter bestimmten Bedingungen in die Datei geschrieben:
- der Puffer ist voll;
- der Dateideskriptor wird mit [close] oder [flush] bearbeitet;
- Zeile 36: Wir erzwingen das Schreiben der Protokollzeile in die Datei. Wir tun dies, weil wir die Protokolle der verschiedenen Threads miteinander vermischt sehen wollen. Wenn wir dies nicht tun, werden die Protokolle eines einzelnen Threads alle gleichzeitig geschrieben – nämlich wenn der Deskriptor in Zeile 45 geschlossen wird. Es wäre dann viel schwieriger zu erkennen, dass bestimmte Threads angehalten wurden: Wir müssten die Zeitstempel in den Protokollen überprüfen;
- Zeile 39: Der Thread1 gibt die ihm zugewiesene Sperre zurück. Sie kann nun einem anderen Thread zugewiesen werden;
- Zeile 22: Die [write]-Methode ist daher synchronisiert: Es schreibt jeweils nur ein Thread in die Logdatei. Der Schlüssel zu diesem Mechanismus ist Zeile 30: Egal, was passiert, nur ein Thread ruft den Schlüssel ab, um zur nächsten Zeile zu gelangen. Er behält ihn, bis er ihn zurückgibt (Zeile 39);
- Zeilen 41–45: Die Methode [close] gibt die dem Log-Dateideskriptor zugewiesenen Ressourcen frei;
Die in die Protokolldatei geschriebenen Protokolle sehen wie folgt aus:
24.2.2. Die Klasse [SendAdminMail]
Die Klasse [SendAdminMail] ermöglicht es Ihnen, bei einem Absturz der Anwendung eine Nachricht an den Anwendungsadministrator zu senden.

Die Klasse [SendAdminMail] wird im Skript [config] [2] wie folgt konfiguriert:
Die Klasse [SendAdminMail] erhält das Wörterbuch aus den Zeilen 2–13 sowie die Konfiguration für den E-Mail-Versand. Die Klasse sieht wie folgt aus:
- Zeilen 24–54: Dies ist der Code, der bereits im Beispiel |smtp/02| behandelt wurde;
- Zeile 20: Wir rufen die Referenz eines Loggers ab. Diese wird in den Zeilen 45 und 49 verwendet;
24.3. Der Webserver
24.3.1. Konfiguration
Die Serverkonfiguration ist der zuvor besprochenen sehr ähnlich. Lediglich die Datei [config.py] hat sich geringfügig geändert:
- Zeilen 40–66: Wir fügen dem Konfigurationswörterbuch des Servers Elemente hinzu, die sich auf den Logger (Zeile 49) und auf das Versenden einer Warn-E-Mail an den Anwendungsadministrator (Zeilen 51–63) beziehen;
- Zeile 65: Um die Threads besser in Aktion zu sehen, werden wir einige von ihnen zwingen, eine Pause einzulegen. [sleep_time] ist die Dauer der Pause in Sekunden;
- Zeilen 27–28: Beachten Sie, dass wir den [index_controller] aus der vorherigen Version 6 verwenden;
24.3.2. Das Hauptskript [main]
Das Hauptskript [main] lautet wie folgt:
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 | |
- Zeilen 1–10: Das Skript erwartet einen Parameter [mysql / pgres], der das zu verwendende DBMS angibt;
- Zeilen 12–14: Die Anwendung wird konfiguriert (Python-Pfad, Layers, Datenbank);
- Zeilen 16–28: Von der Anwendung benötigte Abhängigkeiten;
- Zeilen 30–43: Authentifizierungsmanagement;
- Zeilen 46–51: Eine Funktion, die eine E-Mail an den Anwendungsadministrator sendet;
- Die Funktion erwartet zwei Parameter:
- config: ein Wörterbuch mit den Schlüsseln [adminMail] und [logger];
- die zu versendende Nachricht;
- Zeilen 49–50: Wir bereiten die E-Mail-Konfiguration vor;
- wir versenden die E-Mail;
- Zeilen 54–74: Wir prüfen, ob die Protokolldatei vorhanden ist;
- Zeilen 70–74: Wenn wir die Protokolldatei nicht öffnen konnten, senden wir eine E-Mail an den Administrator und beenden das Programm;
- Zeilen 76–79: Protokollierung des Serverstarts;
- Zeilen 81–98: Wir rufen die Steuerverwaltungsdaten aus der Datenbank ab;
- Zeilen 88–98: Wenn wir diese Daten nicht abrufen konnten, protokollieren wir den Fehler sowohl auf der Konsole als auch in der Protokolldatei;
- Zeilen 100–101: Der Hauptthread protokolliert nicht mehr (die erstellten Threads verwenden nicht denselben Dateideskriptor);
- Zeilen 103–105: Wenn wir keine Verbindung zur Datenbank herstellen konnten, brechen wir ab;
- Zeile 122: Der Server wird im Multithread-Modus gestartet;
Die Funktion [index] (Zeile 114) lautet wie folgt:
- Zeile 4: Die Funktion, die ausgeführt wird, wenn ein Benutzer die URL / anfordert. Da der Server multithreaded ist (Zeile 112), wird ein Thread erstellt, um die Funktion auszuführen. Dieser Thread kann jederzeit unterbrochen und angehalten werden, um die Ausführung etwas später fortzusetzen. Behalte dies immer im Hinterkopf, wenn der Code auf eine Ressource zugreift, die von allen Threads gemeinsam genutzt wird. In diesem Fall ist diese Ressource die Protokolldatei: Alle Threads schreiben in sie;
- Zeile 8: Wir erstellen eine Instanz des Loggers. Somit verfügen alle Threads über eine eigene Instanz des Loggers. Alle diese Logger verweisen jedoch auf dieselbe Logdatei. Es ist dennoch wichtig zu beachten, dass das Schließen des Loggers durch einen Thread keine Auswirkungen auf die Logger der anderen Threads hat;
- Zeilen 9–12: Wir speichern den Logger im [config]-Wörterbuch der Anwendung unter einem Schlüssel, der nach dem Thread benannt ist. Wenn also n Threads gleichzeitig laufen, werden n Einträge im [config]-Wörterbuch erstellt. [config] ist eine Ressource, die von allen Threads gemeinsam genutzt wird. Daher kann eine Synchronisation erforderlich sein. Ich bin hier von einer Annahme ausgegangen. Ich bin davon ausgegangen, dass es keine Auswirkungen hätte, wenn zwei Threads gleichzeitig ihre Einträge in der [config]-Datei erstellen würden und einer von ihnen durch den anderen unterbrochen würde. Der unterbrochene Thread könnte die Erstellung des Eintrags später abschließen. Sollte sich diese Annahme im Test als falsch erweisen, müsste der Zugriff auf Zeile 12 synchronisiert werden;
- Zeile 10: Wir legen den Logger in einem Wörterbuch ab;
- Zeile 11: [threading.current_thread()] ist der Thread, der diese Zeile ausführt, und somit der Thread, der die Funktion [index] ausführt. Wir speichern seinen Namen. Jeder Thread hat einen eindeutigen Namen;
- Zeile 12: Wir speichern die Konfiguration des Threads. Von nun an gehen wir immer wie folgt vor: Wenn es Informationen gibt, die nicht zwischen Threads geteilt werden können, werden sie dennoch in der allgemeinen Konfiguration abgelegt, jedoch mit dem Namen des Threads verknüpft;
- Zeile 14: Wir protokollieren die Anfrage, die wir gerade ausführen;
- Zeilen 15–24: Wir halten bestimmte Threads nach dem Zufallsprinzip an, damit sie den Prozessor an einen anderen Thread abgeben;
- Zeile 16: Wir lesen die Pausendauer (in Sekunden) aus der Konfiguration aus;
- Zeile 17: Eine Pause tritt nur auf, wenn die Pausendauer ungleich 0 ist;
- Zeile 19: eine zufällige ganze Zahl im Bereich [0, 1]. Daher sind nur die Werte 0 und 1 möglich;
- Zeile 20: Der Thread wird nur angehalten, wenn die Zufallszahl 1 ist;
- Zeile 22: Wir protokollieren, dass der Thread kurz vor einer Pause steht;
- Zeile 24: Der Thread wird für [sleep_time] Sekunden angehalten;
- Zeile 26: Wenn der Thread wieder aktiv wird, lässt er das Modul [index_controller] die Anfrage ausführen;
- Zeilen 28–32: Wenn diese Ausführung einen [500 INTERNAL SERVER ERROR] verursacht, wird eine E-Mail an den Administrator gesendet;
- Zeilen 30–31: Wir konfigurieren das [config_mail]-Wörterbuch, das wir an die [SendAdminMail]-Klasse übergeben werden;
- Zeile 32: Die an den Administrator gesendete Nachricht ist die JSON-Zeichenkette des Ergebnisses, das an den Client gesendet wird;
- Zeilen 33–34: Wir protokollieren die Antwort, die an den Client gesendet wird (Zeile 36);
- Zeilen 37–44: Behandlung etwaiger Ausnahmen;
- Zeilen 39–40: Wenn der Logger existiert, protokollieren wir den aufgetretenen Fehler;
- Zeilen 47–48: Wir schließen den Logger, falls er existiert. Letztendlich erstellt der Thread zu Beginn der Anfrage einen Logger und schließt ihn, sobald die Anfrage verarbeitet wurde;
24.3.3. Der Controller [index_controller]
Der Controller [index_controller], der die Anfragen ausführt, ist derselbe wie in der vorherigen Version:

24.3.4. Ausführung
Wir starten den Flask-Server, den E-Mail-Server |hMailServer| und den E-Mail-Client |Thunderbird|. Das DBMS starten wir nicht. Der Server stoppt mit den folgenden Konsolenprotokollen:
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
Die Protokolldatei [logs.txt] lautet wie folgt:
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)]]
Überprüfen Sie mit Thunderbird die E-Mails des Administrators [guest@localhost.com]:

Starten Sie anschließend das DBMS und rufen Sie die URL [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000] auf. Die Protokolle sehen dann wie folgt aus:
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}}}
- Zeilen 1–4: Beachten Sie, dass der Server zweimal startet, da der Modus [Debug=True] einen zweiten Start auslöst;
- Zeilen 5–6: Die Protokolle geben uns einen Eindruck von der Ausführungszeit einer Anfrage, hier 2,293 Millisekunden;
24.4. Der Web-Client

Das Verzeichnis [http-clients/02] wird durch Kopieren des Verzeichnisses [http-clients/01] erstellt. Anschließend nehmen wir einige Änderungen vor.
24.4.1. Die Konfiguration
Die [config]-Konfiguration der Anwendung [http-clients/02] entspricht der der Anwendung [http-clients/01], mit einigen geringfügigen Unterschieden:
- Zeilen 31–32: Wir verwenden denselben Logger |Logger| wie für den Server;
- Zeile 49: der absolute Pfad zur Protokolldatei;
- Zeile 60: Der Modus [debug=True] wird verwendet, um die Antworten des Webservers in die Logdatei zu schreiben;
24.4.2. Die [dao]-Schicht
Der Code für die Klasse [ImpôtsDaoWithHttpClient] ändert sich geringfügig:
- Zeile 17: Wir speichern die allgemeine Konfiguration. Wir werden später sehen, dass beim Ausführen des Konstruktors der Klasse [ImpôtsDaoWithHttpClient] das Wörterbuch [config] den in Zeile 37 verwendeten Schlüssel [logger] noch nicht enthält. Deshalb können wir [self.__logger] (Zeile 23) im Konstruktor nicht initialisieren;
- Zeile 21: Wir haben der Konfiguration einen [debug]-Schlüssel hinzugefügt, der die Protokollierung in den Zeilen 33–39 steuert;
- Zeile 34: wenn wir uns im [debug]-Modus befinden;
- Zeilen 36–37: Optionale Initialisierung der Eigenschaft [self.__logger]. Wenn die Methode [calculate_tax] verwendet wird, ist der Schlüssel [logger] Teil des Wörterbuchs [config];
- Zeile 39: Wir protokollieren das Textdokument, das mit der HTTP-Antwort des Servers verknüpft ist;
Die [dao]-Schicht wird von mehreren Threads gleichzeitig ausgeführt. Hier erstellen wir jedoch eine einzige Instanz dieser Schicht (siehe config_layers). Wir müssen daher sicherstellen, dass der Code keinen Schreibzugriff auf gemeinsam genutzte Daten beinhaltet, typischerweise die Eigenschaften der Klasse [ImpôtsDaoWithHttpClient], die die [dao]-Schicht implementiert. Im obigen Code ändert Zeile 37 jedoch eine Eigenschaft der Klasseninstanz. Hier hat dies keine Konsequenzen, da alle Threads denselben Logger nutzen. Wäre dies nicht der Fall gewesen, hätte der Zugriff auf Zeile 37 synchronisiert werden müssen.
24.4.3. Das Hauptskript
Das Hauptskript [main] verläuft wie folgt:
- Das Hauptskript unterscheidet sich von dem des vorherigen Clients dadurch, dass es mehrere Ausführungsthreads generiert, um Anfragen an den Server zu senden. Der Client in Version 6 sendete alle seine Anfragen nacheinander. Anfrage #i wurde erst gestellt, nachdem die Antwort auf Anfrage #[i-1] empfangen wurde. Hier wollen wir sehen, wie sich der Server verhält, wenn er mehrere gleichzeitige Anfragen erhält. Dazu benötigen wir Threads;
- Zeile 21: Die generierten Threads werden in einer Liste abgelegt. Es ist wichtig zu verstehen, dass das [main]-Skript ebenfalls von einem Thread namens [MainThread] ausgeführt wird. Dieser Haupt-Thread erstellt weitere Threads, die für die Berechnung der Steuer für einen oder mehrere Steuerzahler zuständig sind;
- Zeile 26: Wir erstellen einen Logger. Dieser wird von allen Threads gemeinsam genutzt;
- Zeile 32: Wir rufen alle Steuerzahler ab, deren Steuern berechnet werden müssen;
- Zeilen 39–51: Wir verteilen diese Steuerzahler auf mehrere Threads;
- Zeilen 40–41: Jeder Thread verarbeitet 1 bis 4 Steuerzahler. Diese Anzahl wird zufällig bestimmt;
- [random.randint(1, 4)] generiert zufällig eine Zahl aus der Liste [1, 2, 3, 4];
- Der Thread darf nicht mehr als [l-i] Steuerzahler umfassen, wobei [l-i] die Anzahl der Steuerzahler angibt, denen noch kein Thread zugewiesen wurde;
- wir nehmen daher den kleineren der beiden Werte;
- Zeile 43: Sobald [nb_taxpayers], die Anzahl der vom Thread verarbeiteten Steuerzahler, bekannt ist, entnehmen wir diese der Liste der Steuerzahler:
- [slice(10,12)] ist die Menge der Indizes [10, 11, 12];
- [taxpayers[slice(10,12)]] ist die Liste [taxpayers[10], taxpayers[11], taxpayers[12] ;
- Zeile 45: Wir erhöhen den Wert von i, der die Schleife in Zeile 39 steuert;
- Zeile 47: Wir erstellen einen Thread:
- [target=thread_function] legt die Funktion fest, die der Thread ausführen wird. Dies ist die Funktion aus den Zeilen 16–17. Sie erwartet drei Parameter;
- [ags] ist die Liste der drei Parameter, die von der Funktion [thread_function] erwartet werden;
Das Erstellen eines Threads führt diesen nicht aus. Es wird lediglich ein Objekt erstellt;
- Zeilen 48–49: Der soeben erstellte Thread wird der Liste der vom Hauptthread erstellten Threads hinzugefügt;
- Zeile 51: Der Thread wird gestartet. Er läuft dann parallel zu den anderen aktiven Threads. Hier führt er die [thread_function] mit den ihm übergebenen Argumenten aus;
- Zeilen 53–54: Der Hauptthread wartet auf jeden der von ihm gestarteten Threads. Nehmen wir ein Beispiel:
- Der Hauptthread hat drei Threads gestartet [th1, th2, th3];
- Der Hauptthread wartet auf jeden der Threads (Zeilen 53–54) in der Reihenfolge der for-Schleife: [th1, th2, th3];
- Angenommen, die Threads werden in der Reihenfolge [th2, th1, th3] beendet;
- Der Hauptthread wartet darauf, dass th1 beendet wird. Wenn th2 beendet wird, passiert nichts;
- Wenn th1 beendet ist, wartet der Hauptthread auf th2. th2 ist jedoch bereits beendet. Der Hauptthread fährt dann mit dem nächsten Thread fort und wartet auf th3;
- Wenn th3 beendet ist, hat der Hauptthread das Warten beendet und fährt mit der Ausführung von Zeile 57 fort;
- Zeile 57 schreibt die Ergebnisse in die Ergebnisdatei. Dies ist ein gutes Beispiel für Objektreferenzen:
- Zeile 43: Die einem Thread zugeordnete Liste [thread_payers] enthält Kopien der Objektreferenzen, die in der Liste [taxpayers] enthalten sind;
- wir wissen, dass die Steuerberechnung die Objekte ändern wird, auf die die Referenzen in der Liste [thread_payers] verweisen. Diese Objekte werden mit den Ergebnissen der Steuerberechnung aktualisiert. Die Referenzen selbst werden jedoch nicht geändert. Daher „sehen“ die Referenzen in der ursprünglichen Liste [taxpayers] die geänderten Objekte oder „verweisen“ auf sie;
Die von den Threads ausgeführte [thread_function] lautet wie folgt:
- Funktionen, die von mehreren Threads gleichzeitig ausgeführt werden, sind oft schwierig zu schreiben: Sie müssen stets sicherstellen, dass der Code nicht versucht, Daten zu ändern, die von mehreren Threads gemeinsam genutzt werden. In diesem Fall müssen Sie einen synchronisierten Zugriff auf die gemeinsam genutzten Daten implementieren, die geändert werden sollen;
- Zeile 3: Die Funktion erhält drei Parameter:
- [dao]: eine Referenz auf die [dao]-Schicht. Diese Daten werden gemeinsam genutzt;
- [logger]: eine Referenz auf den Logger. Diese Daten werden gemeinsam genutzt;
- [taxpayers]: eine Liste von Steuerzahlern. Diese Daten werden nicht gemeinsam genutzt: Jeder Thread verwaltet eine eigene Liste;
- Betrachten wir die beiden Referenzen [dao, logger]:
- Wir haben gesehen, dass das Objekt, auf das die Referenz [dao] zeigte, eine Referenz [self.__logger] hatte, die von den Threads geändert wurde, allerdings um einen Wert festzulegen, der für alle Threads gleich ist;
- Die Referenz [logger] verweist auf einen Dateideskriptor. Wir haben gesehen, dass es beim Schreiben von Protokollen in die Datei zu Problemen kommen kann. Aus diesem Grund wurde das Schreiben in die Datei synchronisiert;
- Zeilen 5–6: Wir protokollieren den Namen des Threads und die Anzahl der Steuerzahler, die er verwalten muss;
- Zeilen 8–14: Berechnung der Steuern der Steuerzahler;
- Zeile 16: Protokollierung des Endes des Threads;
24.4.4. Ausführung
Starten wir den Webserver wie im vorherigen Abschnitt beschrieben (Webserver, DBMS, hMailServer, Thunderbird) und führen wir dann das [main]-Skript des Clients aus. In den Dateien [data/output/errors.txt, data/output/results.json] erhalten wir die gleichen Ergebnisse wie in der vorherigen Version. In der Datei [data/logs/logs.txt] finden wir folgende Protokolle:
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]
- Diese Protokolle zeigen, dass fünf Threads gestartet wurden, um die Steuern für 11 Steuerzahler zu berechnen. Diese fünf Threads haben gleichzeitig Anfragen an den Steuerberechnungsserver gesendet. Es ist wichtig zu verstehen, wie dies funktioniert:
- Thread [Thread-1] wird als erster gestartet. Wenn er die CPU-Ressourcen hat, führt er den Code aus, bis er seine HTTP-Anfrage sendet. Da er auf das Ergebnis dieser Anfrage warten muss, wird er automatisch in den Wartezustand versetzt. Er verliert dann die CPU-Ressourcen, und ein anderer Thread übernimmt;
- Zeilen 1–10: Der gleiche Vorgang wiederholt sich für jeden der 5 Threads. Somit werden die 5 Threads gestartet, noch bevor Thread [Thread-1] in Zeile 11 überhaupt seine Antwort erhalten hat;
- Die Threads werden nicht in der Reihenfolge beendet, in der sie gestartet wurden. Somit endet Thread [Thread-3] als erster, Zeile 23;
Auf der Serverseite sehen die Protokolle in der Datei [data/logs/logs.txt] wie folgt aus:
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}}}
- Wir sehen, dass 11 Threads die 11 Steuerzahler bearbeitet haben;
- einige Threads wurden angehalten (Zeilen 6, 8, 12, 14, 20, 22) und andere nicht (Zeilen 9, 23, 25, 29, 31);
24.5. [DAO]-Schicht-Tests
Wie bereits in der |vorherigen Version| testen wir die [DAO]-Schicht des Clients. Das Prinzip ist genau dasselbe:

Die Testklasse wird in der folgenden Umgebung ausgeführt:

- Die Konfiguration [2] ist identisch mit der Konfiguration [1], die wir gerade untersucht haben;
Die Testklasse [TestHttpClientDao] sieht wie folgt aus:
- Wir erstellen eine |Ausführungskonfiguration| für diesen Test;
- Wir starten den Webserver mit seiner gesamten Umgebung;
- führen den Test aus;
Die Ergebnisse lauten wie folgt:
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