24. تمرين عملي: الإصدار 7
24.1. مقدمة
الإصدار 7 من تطبيق حساب الضرائب مطابق للإصدار 6 باستثناء التفاصيل التالية:
- سيقوم عميل الويب بإرسال عدة طلبات HTTP في وقت واحد. في الإصدار السابق، كانت هذه الطلبات تُرسل بالتتابع. وبالتالي، كان بإمكان الخادم معالجة طلب واحد فقط في كل مرة؛
- سيكون الخادم متعدد الخيوط: سيكون قادرًا على معالجة طلبات متعددة في وقت واحد؛
- لتتبع تنفيذ هذه الطلبات، سيتم تزويد خادم الويب بمسجل يقوم بتسجيل اللحظات الرئيسية في معالجة الطلبات في ملف نصي؛
- سيقوم الخادم بإرسال بريد إلكتروني إلى مسؤول التطبيق عندما يواجه مشكلة تمنعه من البدء، وعادةً ما تكون هذه المشكلة متعلقة بقاعدة البيانات المرتبطة بخادم الويب؛
تظل بنية التطبيق دون تغيير:

هيكل دليل البرامج النصية كما يلي:

يتم إنشاء المجلد [http-servers/02] أولاً عن طريق نسخ المجلد [http-servers/01]. ثم يتم إجراء تعديلات عليه.
24.2. الأدوات المساعدة

24.2.1. فئة [Logger]
تسمح فئة [Logger] بتسجيل إجراءات معينة لخادم الويب في ملف نصي:
- السطران 10-11: نُعرِّف سمة فئة. سمة الفئة هي خاصية مشتركة بين جميع مثيلات الفئة. ويُشار إليها باستخدام الترميز [Class.class_attribute] (السطران 30 و39). وستعمل سمة الفئة [lock] ككائن تزامن لجميع الخيوط التي تُنفِّذ الكود الوارد في الأسطر 31-36؛
- الأسطر 14-19: يتلقى المنشئ المسار المطلق لملف السجل. ثم يتم فتح هذا الملف، ويتم تخزين واصف الملف الذي تم استرداده في الفئة؛
- السطر 17: يتم فتح ملف السجل في وضع "الإضافة" (a). سيتم إضافة كل سطر مكتوب إلى نهاية الملف؛
- الأسطر 22-39: تسمح طريقة [write] بكتابة رسالة تم تمريرها كمعلمة إلى ملف السجل. يتم إلحاق معلومتين بهذه الرسالة:
- السطر 24: التاريخ الحالي؛
- السطر 25: الوقت الحالي؛
- السطر 27: اسم الخيط الذي يكتب السجل. من المهم أن نتذكر هنا أن تطبيق الويب يخدم عدة مستخدمين في وقت واحد. يتم تعيين خيط لكل طلب لتنفيذه. إذا تم إيقاف هذا الخيط مؤقتًا — عادةً لعملية إدخال/إخراج (الشبكة، الملفات، قاعدة البيانات) — يتم تسليم المعالج إلى خيط آخر. بسبب هذه الانقطاعات المحتملة، لا يمكننا التأكد من أن الخيط سينجح في كتابة سطر في ملف السجل دون أن يتعرض للانقطاع. لذلك، هناك خطر من اختلاط السجلات من خيطين مختلفين. هذا الخطر منخفض، وربما يكون معدومًا، لكننا قررنا مع ذلك إظهار كيفية مزامنة وصول خيطين إلى مورد مشترك، وهو في هذه الحالة ملف السجل؛
- السطر 30: قبل الكتابة، يطلب الخيط مفتاح باب الدخول. المفتاح المطلوب هو الذي تم إنشاؤه في السطر 11. وهو بالفعل فريد: سمة الفئة فريدة لجميع مثيلات الفئة؛
- في الوقت T1، يحصل مؤشر ترابط باسم Thread1 على المفتاح. يمكنه بعد ذلك تنفيذ السطر 33؛
- في الوقت T2، يتم إيقاف مؤقتًا مؤشر الترابط Thread1 قبل أن ينتهي حتى من كتابة السجل؛
- في الوقت T3، يجب على الخيط Thread2، الذي حصل على المعالج، كتابة سجل أيضًا. وبذلك يصل إلى السطر 30، حيث يطلب مفتاح الباب الأمامي. ويتم إخباره أن خيطًا آخر يمتلكه بالفعل. ثم يتم إيقافه مؤقتًا تلقائيًا. وسيكون هذا هو الحال بالنسبة لجميع الخيوط التي تطلب هذا المفتاح؛
- في الوقت T4، يستعيد الخيط Thread1، الذي كان متوقفًا مؤقتًا، المعالج. ثم ينهي كتابة السجل؛
- الأسطر 32-36: تتم الكتابة إلى ملف السجل على خطوتين:
- السطر 33: يعمل واصف الملف الذي تم الحصول عليه في السطر 17 مع مخزن مؤقت. تكتب عملية [الكتابة] في السطر 33 إلى هذا المخزن المؤقت وليس مباشرة إلى الملف. ثم يتم تفريغ المخزن المؤقت إلى الملف في ظل ظروف معينة:
- عندما يمتلئ المخزن المؤقت؛
- يخضع واصف الملف لعملية [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. خادم الويب
24.3.1. التكوين
تشبه تكوينات الخادم إلى حد كبير تلك التي تمت مناقشتها سابقًا. لم يتغير سوى ملف [config.py] بشكل طفيف:
- الأسطر 40–66: نضيف العناصر المتعلقة بمسجل الأحداث (السطر 49) وتلك المتعلقة بإرسال بريد إلكتروني تنبيهي إلى مسؤول التطبيق (الأسطر 51–63) إلى قاموس تكوين الخادم؛
- السطر 65: لرؤية الخيوط أثناء العمل بشكل أفضل، سنقوم بإجبار بعضها على التوقف مؤقتًا. [sleep_time] هي مدة التوقف المؤقت معبراً عنها بالثواني؛
- الأسطر 27–28: لاحظ أننا نستخدم [index_controller] من الإصدار 6 السابق؛
24.3.2. البرنامج النصي الرئيسي [main]
النص البرمجي الرئيسي [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] تحدد نظام إدارة قواعد البيانات (DBMS) المطلوب استخدامه؛
- الأسطر 12–14: يتم تكوين التطبيق (مسار Python، الطبقات، قاعدة البيانات)؛
- الأسطر 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: نقوم بإنشاء مثيل لـ logger. وبالتالي، سيكون لكل مؤشر ترابط مثيل مختلف لـ logger. ومع ذلك، تشير جميع أدوات التسجيل هذه إلى ملف السجل نفسه. لا يزال من المهم ملاحظة أنه عندما يغلق مؤشر ترابط ما أداة التسجيل الخاصة به، فإن هذا لا يؤثر على أدوات التسجيل الخاصة بمؤشرات الترابط الأخرى؛
- الأسطر 9–12: نقوم بتخزين المسجل في قاموس [config] الخاص بالتطبيق تحت مفتاح يحمل اسم الخيط. وبالتالي، إذا كان هناك n خيوط تعمل في وقت واحد، فسيتم إنشاء n إدخالات في قاموس [config]. [config] هو مورد مشترك بين جميع الخيوط. لذلك، قد تكون هناك حاجة إلى التزامن. لقد قمت بافتراض هنا. افترضت أنه إذا أنشأ خيطان في نفس الوقت إدخالاتهما في ملف [config] وتوقف أحدهما بسبب الآخر، فلن يكون لذلك أي تأثير. يمكن للخيط الذي توقف أن يكمل إنشاء الإدخال لاحقًا. إذا أظهر الاختبار أن هذا الافتراض خاطئ، فسيكون من الضروري مزامنة الوصول إلى السطر 12؛
- السطر 10: نضع المسجل في قاموس؛
- السطر 11: [threading.current_thread()] هو الخيط الذي ينفذ هذا السطر، وبالتالي الخيط الذي ينفذ وظيفة [index]. نسجل اسمه. لكل خيط اسم فريد؛
- السطر 12: نقوم بتخزين تكوين الخيط. من الآن فصاعدًا، سنقوم دائمًا بما يلي: إذا كانت هناك معلومات لا يمكن مشاركتها بين الخيوط، فسيتم وضعها في التكوين العام، ولكن مرتبطة باسم الخيط؛
- السطر 14: نقوم بتسجيل الطلب الذي نقوم بتنفيذه حاليًا؛
- الأسطر 15-24: نقوم بإيقاف مؤقت لبعض الخيوط بشكل عشوائي حتى تفسح المجال للمعالج لخيوط أخرى؛
- السطر 16: نستخرج مدة التوقف (بالثواني) من ملف التكوين؛
- السطر 17: لا تحدث وقفة إلا إذا كانت مدة الوقفة غير صفر؛
- السطر 19: عدد صحيح عشوائي في النطاق [0، 1]. لذلك، لا يمكن أن تكون القيم إلا 0 و 1؛
- السطر 20: يتم إيقاف مؤقتًا مؤشر الترابط فقط إذا كان الرقم العشوائي هو 1؛
- السطر 22: نسجل حقيقة أن الخيط على وشك التوقف مؤقتًا؛
- السطر 24: يتم إيقاف مؤقتًا الخيط لمدة [sleep_time] ثانية؛
- السطر 26: عندما يستيقظ الخيط، يقوم الوحدة النمطية [index_controller] بتنفيذ الطلب؛
- الأسطر 28–32: إذا تسبب هذا التنفيذ في ظهور [500 INTERNAL SERVER ERROR]، يتم إرسال بريد إلكتروني إلى المسؤول؛
- السطران 30-31: نقوم بتكوين قاموس [config_mail] الذي سنمرره إلى فئة [SendAdminMail]؛
- السطر 32: الرسالة المرسلة إلى المسؤول هي سلسلة JSON للنتيجة التي سيتم إرسالها إلى العميل؛
- الأسطر 33–34: نقوم بتسجيل الاستجابة التي سيتم إرسالها إلى العميل (السطر 36)؛
- الأسطر 37-44: معالجة أي استثناءات؛
- السطران 39-40: إذا كان المسجل موجودًا، نقوم بتسجيل الخطأ الذي حدث؛
- السطران 47-48: نغلق المسجل إذا كان موجودًا. في النهاية، يقوم الخيط بإنشاء مسجل في بداية الطلب ويغلقه بمجرد معالجة الطلب؛
24.3.3. وحدة التحكم [index_controller]
وحدة التحكم [index_controller] التي تنفذ الطلبات هي نفسها الموجودة في الإصدار السابق:

24.3.4. التنفيذ
نقوم بتشغيل خادم Flask وخادم البريد الإلكتروني |hMailServer| وعميل البريد الإلكتروني |Thunderbird|. لا نقوم بتشغيل نظام إدارة قواعد البيانات. يتوقف الخادم مع ظهور سجلات وحدة التحكم التالية:
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]:

ثم قم بتشغيل نظام إدارة قواعد البيانات (DBMS) واطلب عنوان 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. عميل الويب

يتم إنشاء الدليل [http-clients/02] عن طريق نسخ الدليل [http-clients/01]. ثم نقوم بإجراء بعض التعديلات.
24.4.1. التكوين
تكوين [config] لتطبيق [http-clients/02] هو نفسه تكوين تطبيق [http-clients/01]، مع بعض الاختلافات الطفيفة:
- السطران 31-32: سنستخدم نفس المسجل |Logger| المستخدم للخادم؛
- السطر 49: المسار المطلق لملف السجل؛
- السطر 60: يُستخدم الوضع [debug=True] لكتابة استجابات خادم الويب في ملف السجل؛
24.4.2. طبقة [dao]
يتغير كود فئة [ImpôtsDaoWithHttpClient] قليلاً:
- السطر 17: نقوم بتخزين التكوين العام. سنرى لاحقًا أنه عند تشغيل منشئ فئة [ImpôtsDaoWithHttpClient]، لا يحتوي قاموس [config] بعد على المفتاح [logger] المستخدم في السطر 37. ولهذا السبب لا يمكننا تهيئة [self.__logger] (السطر 23) في المنشئ؛
- السطر 21: أضفنا مفتاح [debug] إلى التكوين الذي يتحكم في التسجيل في الأسطر 33-39؛
- السطر 34: إذا كنا في وضع [debug]؛
- السطور 36-37: تهيئة اختيارية لخاصية [self.__logger]. عند استخدام طريقة [calculate_tax]، يكون المفتاح [logger] جزءًا من القاموس [config]؛
- السطر 39: نقوم بتسجيل المستند النصي المرتبط باستجابة HTTP للخادم؛
سيتم تنفيذ طبقة [dao] في وقت واحد بواسطة خيوط متعددة. ومع ذلك، نقوم هنا بإنشاء مثيل واحد لهذه الطبقة (انظر config_layers). لذلك يجب علينا التحقق من أن الكود لا يتضمن وصولًا للكتابة إلى البيانات المشتركة، وعادةً ما تكون خصائص فئة [ImpôtsDaoWithHttpClient] التي تنفذ طبقة [dao]. ومع ذلك، في الكود أعلاه، يقوم السطر 37 بتعديل خاصية لمثيل الفئة. وهنا، لا يترتب على ذلك أي عواقب لأن جميع الخيوط تشترك في نفس المسجل. ولو لم يكن الأمر كذلك، لكان من الضروري مزامنة الوصول إلى السطر 37.
24.4.3. النص البرمجي الرئيسي
يتطور البرنامج النصي الرئيسي [main] على النحو التالي:
- يختلف البرنامج النصي الرئيسي عن البرنامج النصي للعميل السابق في أنه سيقوم بإنشاء خيوط تنفيذ متعددة لإرسال الطلبات إلى الخادم. كان العميل في الإصدار 6 يرسل جميع طلباته بالتسلسل. لم يتم إرسال الطلب رقم #i إلا بعد استلام الرد على الطلب رقم #[i-1]. هنا، نريد أن نرى كيف يتصرف الخادم عند استلامه لطلبات متعددة في وقت واحد. ولهذا، نحتاج إلى خيوط؛
- السطر 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]؛
- ينتظر الخيط الرئيسي كل خيط من الخيوط (السطران 53-54) بترتيب حلقة for: [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. التنفيذ
لنبدأ تشغيل خادم الويب كما هو موضح في القسم السابق (خادم الويب، 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: تتكرر نفس العملية لكل خيط من الخيوط الخمسة. وبالتالي، يتم تشغيل الخيوط الخمسة قبل أن يتلقى الخيط [Thread-1] ردّه في السطر 11؛
- لا تنتهي الخيوط بالترتيب الذي تم تشغيلها به. وبالتالي، ينتهي الخيط [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] هي كما يلي:
- نقوم بإنشاء |تكوين تنفيذ| لهذا الاختبار؛
- نقوم بتشغيل خادم الويب مع بيئته بالكامل؛
- نقوم بتشغيل الاختبار؛
والنتائج هي كما يلي:
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