Skip to content

10. خيوط التنفيذ

10.1. فئة Thread

عند تشغيل أحد التطبيقات، فإنه يعمل ضمن تدفق تنفيذ يُسمى مؤشر ترابط. فئة .NET التي تمثل نموذجًا لمؤشر الترابط هي System.Threading.Thread ولها التعريف التالي:

الشركات المصنعة

في الأمثلة التالية، سنستخدم فقط المنشئات [1،3]. تقبل المنشئة [1] كمعلمة طريقة ذات توقيع [2]، أي مع معلمة من نوع object ولا تُرجع أي نتيجة. تقبل المنشئة [3] كمعلمة طريقة ذات توقيع [4]، أي التي لا تحتوي على معلمة ولا تُرجع أي نتيجة.

الخصائص

بعض الخصائص المفيدة:

  • Thread CurrentThread : خاصية ثابتة تعطي مرجعًا إلى الخيط الذي يوجد فيه الكود الذي يطلب هذه الخاصية
  • string Name: اسم الخيط
  • bool IsAlive: تشير إلى ما إذا كان الخيط قيد التشغيل أم لا.

الطرق

الطرق الأكثر استخدامًا هي:

  • Start()، Start(object obj): تبدأ التنفيذ غير المتزامن للخيط، ربما عن طريق تمرير المعلومات إليه في كائن.
  • Abort()، Abort(object obj): لإنهاء مؤشر الترابط قسريًا
  • Join() : يتم حظر الخيط T1 الذي ينفذ T2.Join حتى انتهاء T2. هناك متغيرات لإنهاء الانتظار بعد وقت محدد.
  • Sleep(int n) : طريقة ثابتة - يتم تعليق الخيط الذي ينفذ الطريقة لمدة n مللي ثانية. ثم يفقد المعالج، الذي يُعطى لخيط آخر.

دعونا نلقي نظرة على أول تطبيق يوضح وجود مؤشر ترابط رئيسي للتنفيذ، وهو المؤشر الذي تحتوي عليه الدالة Main لفئة ما:


using System;
using System.Threading;
 
namespace Chap8 {
    class Program {
        static void Main(string[] args) {
             // init current thread
            Thread main = Thread.CurrentThread;
             // display
            Console.WriteLine("Thread courant : {0}", main.Name);
             // we change the name
            main.Name = "main";
             // check
            Console.WriteLine("Thread courant : {0}", main.Name);
 
             // infinite loop
            while (true) {
                 // display
                Console.WriteLine("{0} : {1:hh:mm:ss}", main.Name, DateTime.Now);
                 // temporary shutdown
                Thread.Sleep(1000);
             }//while         
        }
    }
}
  • السطر 8: استرداد مرجع إلى مؤشر الترابط الذي تعمل فيه الطريقة [main]
  • الأسطر 10-14: عرض وتعديل اسمه
  • الأسطر 17-22: حلقة تعرض رسالة كل ثانية
  • السطر 21: سيتم تعليق الخيط الذي تعمل فيه طريقة [main] لمدة ثانية واحدة

نتائج الشاشة هي كما يلي:

1
2
3
4
5
6
7
8
Thread courant :
Thread courant : main
main : 04:19:00
main : 04:19:01
main : 04:19:02
main : 04:19:03
main : 04:19:04
^CAppuyez sur une touche pour continuer...
  • السطر 1: لم يكن للخيط الحالي اسم
  • السطر 2: لديه اسم
  • الأسطر 3-7: العرض كل ثانية
  • السطر 8: تم إيقاف البرنامج بواسطة Ctrl-C.

10.2. إنشاء خيوط التنفيذ

من الممكن أن تكون هناك تطبيقات تعمل فيها أجزاء من الكود "بشكل متزامن" في خيوط تنفيذ مختلفة. عندما نقول أن الخيوط تعمل بشكل متزامن، فغالبًا ما يكون هذا مصطلحًا خاطئًا. إذا كان الجهاز يحتوي على معالج واحد فقط، كما هو الحال في كثير من الأحيان، فإن الخيوط تتشارك هذا المعالج: حيث يمكن لكل منها الوصول إليه، بالتناوب، لفترة قصيرة (بضع ميلي ثوانٍ). وهذا يعطي انطباعًا زائفًا بالتنفيذ المتوازي. يعتمد الجزء من الوقت المخصص لخيط على عوامل مختلفة، بما في ذلك أولويته، التي لها قيمة افتراضية ولكن يمكن أيضًا تعيينها برمجيًا. عندما يكون الخيط هو صاحب المعالج، فإنه يستخدمه بشكل طبيعي طوال الوقت المخصص له. ومع ذلك، يمكنه تحريره مبكرًا:

  • عن طريق انتظار حدث (Wait، Join)
  • عن طريق وضع نفسه في حالة سكون لفترة زمنية محددة (Sleep)
  1. يتم إنشاء مؤشر الترابط T أولاً بواسطة أحد المُصنّعين المذكورين أعلاه، على سبيل المثال:
Thread thread=new Thread(Start);

حيث Start هي طريقة ذات أحد التوقيعين التاليين:

void Start();
void Start(object obj);

إنشاء مؤشر ترابط لا يعني تشغيله.

  1. يتم تشغيل الخيط T بواسطة T.Start() : وعندئذٍ سيقوم الخيط T بتنفيذ الطريقة Start التي تم تمريرها إلى مُنشئ T. ولا ينتظر البرنامج الذي ينفذ T.Start() انتهاء المهمة T: بل ينتقل فورًا إلى التعليمات التالية. وهذا يعني أن مهمتين تعملان بالتوازي. وفي كثير من الحالات، يتعين أن تكونا قادرتين على التواصل مع بعضهما البعض لمتابعة تقدم عملهما المشترك. وهذه هي مشكلة تزامن الخيوط.
  2. بمجرد إطلاقه، يعمل الخيط T بشكل مستقل. سيتوقف عندما تنتهي المهمة التي ينفذها Start من عملها.
  3. يمكن إجبار مؤشر الترابط T على الإنهاء:
    1. يطلب T.Abort() من الخيط T الإنهاء.
  4. يمكنك أيضًا انتظار انتهاء تنفيذه بواسطة T.Join(). هذه تعليمة حجب: يتم حجب البرنامج الذي ينفذها حتى تكمل المهمة T عملها. هذه وسيلة للتزامن.

دعونا نلقي نظرة على البرنامج التالي:


using System;
using System.Threading;
 
namespace Chap8 {
    class Program {
        public static void Main() {
             // init Current thread
            Thread main = Thread.CurrentThread;
             // name the Thread
            main.Name = "Main";
 
             // creation of execution threads
            Thread[] tâches = new Thread[5];
            for (int i = 0; i < tâches.Length; i++) {
                 // create thread i
                tâches[i] = new Thread(Affiche);
                 // set the thread name
                tâches[i].Name =  i.ToString();
                 // start execution of thread i
                tâches[i].Start();
            }
 
             // end of hand
            Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}",main.Name,DateTime.Now);
        }
 
        public static void Affiche() {
             // display start of execution
            Console.WriteLine("Début d'exécution de la méthode Affiche dans le Thread {0} : {1:hh:mm:ss}",Thread.CurrentThread.Name,DateTime.Now);
             // sleep for 1 s
            Thread.Sleep(1000);
             // display end of run
            Console.WriteLine("Fin d'exécution de la méthode Affiche dans le Thread {0} : {1:hh:mm:ss}", Thread.CurrentThread.Name, DateTime.Now);
        }
    }
}
  • الأسطر 8-10: تسمية الخيط الذي ينفذ الطريقة [Main]
  • الأسطر 13-21: يتم إنشاء 5 خيوط وتنفيذها. يتم تخزين مراجع الخيوط في مصفوفة لاسترجاعها لاحقًا. يقوم كل خيط بتنفيذ الأسطر 27-35 من Poster.
  • السطر 20: يتم بدء تشغيل الخيط رقم i. هذه العملية غير معطلة. سيتم تشغيل الخيط رقم i بالتوازي مع خيط طريقة [Main] الذي أطلقه.
  • السطر 24: ينتهي الخيط الذي ينفذ طريقة [Main].
  • الأسطر 27-35: تقوم طريقة [Display] بالعرض. تعرض اسم الخيط الذي ينفذها، بالإضافة إلى أوقات بدء وانتهاء التنفيذ.
  • السطر 31: سيتوقف أي مؤشر ترابط يقوم بتنفيذ طريقة [Display] لمدة ثانية واحدة. ثم سيتم منح المعالج لمؤشر ترابط آخر ينتظر المعالج. في نهاية ثانية التوقف، سيكون مؤشر الترابط المتوقف مرشحًا للمعالج. وسيحصل عليه عندما يحين دوره. يعتمد هذا على عوامل مختلفة، بما في ذلك أولوية مؤشرات الترابط الأخرى التي تنتظر المعالج.

والنتائج هي كما يلي:

Début d'exécution de la méthode Affiche dans le Thread 0 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 1 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 2 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 3 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 4 : 10:30:44
Fin du thread Main à 10:30:44
Fin d'exécution de la méthode Affiche dans le Thread 0 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 1 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 2 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 3 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 4 : 10:30:45

هذه النتائج مفيدة للغاية:

  • أولاً، يمكننا أن نرى أن بدء تنفيذ مؤشر ترابط لا يؤدي إلى حجب. فقد بدأ Main تنفيذ 5 مؤشرات ترابط بالتوازي وأكمل تنفيذه قبلها. العملية

                // on lance l'exécution du thread i
                tâches[i].Start();

يبدأ تنفيذ الخيط tasks[i] ولكن بمجرد الانتهاء من ذلك، يستمر التنفيذ فورًا مع التعليمات التالية، دون انتظار انتهاء الخيط من التنفيذ.

  • يجب أن تنفذ جميع الخيوط التي تم إنشاؤها Affiche. ترتيب التنفيذ غير متوقع. على الرغم من أن ترتيب التنفيذ في المثال يبدو أنه يتبع ترتيب طلبات التنفيذ، لا يمكن استخلاص أي استنتاجات عامة من هذا. يحتوي نظام التشغيل هنا على 6 خيوط ومعالج واحد. سيقوم بتوزيع المعالج على هذه الخيوط الست وفقًا لقواعده الخاصة.
  • النتائج هي نتيجة لطريقة Sleep. في المثال، الخيط 0 هو أول من ينفذ طريقة Affiche. يتم عرض رسالة بدء التنفيذ، ثم يتم تنفيذ Sleep الذي يعلقه لمدة ثانية واحدة. ثم يفقد المعالج، الذي يصبح متاحًا لخيط آخر. يوضح المثال أن الخيط 1 سيحصل عليه. سيتبع الخيط 1 نفس مسار الخيوط الأخرى. عندما تنتهي ثانية النوم للخيط 0، يمكن استئناف تنفيذه. يمنحه النظام المعالج ويمكنه إنهاء تنفيذ طريقة Affiche.

دعونا نعدل برنامجنا لإنهاء الدالة Main باستخدام التعليمات التالية:


             // end of hand
            Console.WriteLine("Fin du thread " + main.Name);
             // stop all threads
Environment.Exit(0);

يؤدي تشغيل البرنامج الجديد إلى النتائج التالية:

1
2
3
4
5
6
Début d'exécution de la méthode Affiche dans le Thread 0 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 1 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 2 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 3 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 4 : 10:33:18
Fin du thread Main à 10:33:18
  • السطور 1-5: تبدأ الخيوط التي أنشأها Main التنفيذ ويتم مقاطعتها لمدة ثانية واحدة
  • السطر 6: يستعيد مؤشر الترابط [Main] المعالج وينفذ التعليمات:
        Environment.Exit(0);

توقف هذه التعليمات جميع الخيوط وليس Main فقط.

إذا أرادت الخيط الرئيسي (Main) انتظار انتهاء تنفيذ الخيوط التي أنشأتها، فيمكنها استخدام فئة Join من فئة Thread:


        public static void Main() {
...
             // we wait for all threads
            for (int i = 0; i < tâches.Length; i++) {
                 // wait for thread i to finish execution
                tâches[i].Join();
            }
             // end of hand
            Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
}
  • السطر 6: ينتظر مؤشر الترابط [Main] كل مؤشر ترابط. يتم حظره أولاً في انتظار مؤشر الترابط رقم 1، ثم مؤشر الترابط رقم 2، وهكذا... وأخيرًا، عند الخروج من حلقة الأسطر 2-5، تكون جميع مؤشرات الترابط الخمسة التي بدأها قد انتهت.

النتائج هي كما يلي:

Début d'exécution de la méthode Affiche dans le Thread 0 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 1 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 2 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 3 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 4 : 10:35:18
Fin d'exécution de la méthode Affiche dans le Thread 0 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 1 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 2 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 3 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 4 : 10:35:19
Fin du thread Main à 10:35:19
  • السطر 11: انتهى مؤشر الترابط [Main] بعد انتهاء مؤشرات الترابط التي بدأها.

10.3. فوائد الخيوط

الآن بعد أن أبرزنا وجود مؤشر ترابط افتراضي، وهو الذي ينفذ Main، ونعرف كيفية إنشاء مؤشرات ترابط جديدة، دعونا نلقي نظرة على ما تعنيه مؤشرات الترابط بالنسبة لنا ولماذا نقدمها هنا. هناك نوع واحد من التطبيقات يناسب استخدام مؤشرات الترابط بشكل جيد، وهو تطبيقات العميل-الخادم على الإنترنت. سنقدمها في الفصل التالي. في تطبيق العميل-الخادم على الإنترنت، يستجيب خادم على الجهاز S1 لطلبات العملاء على الأجهزة البعيدة C1، C2، ...، Cn.

نستخدم يوميًا تطبيقات الإنترنت التي تتوافق مع هذا الرسم التخطيطي: خدمات الويب، والبريد الإلكتروني، وتصفح المنتديات، ونقل الملفات... في الرسم التخطيطي أعلاه، يجب أن يخدم الخادم S1 عملاء Ci في وقت واحد. إذا أخذنا مثال خادم FTP (بروتوكول نقل الملفات) الذي يوصل الملفات إلى عملائه، فإننا نعلم أن نقل الملفات قد يستغرق أحيانًا عدة دقائق. وبالطبع، من المستحيل أن يحتكر عميل ما الخادم طوال هذه المدة. ما يحدث عادةً هو أن يقوم الخادم بإنشاء عدد من سلاسل التنفيذ يساوي عدد العملاء. ثم يتولى كل سلسلة التعامل مع عميل معين. ونظرًا لأن المعالج يتم تقاسمه بشكل دوري بين جميع سلاسل الجهاز النشطة، فإن الخادم يقضي وقتًا قصيرًا مع كل عميل، مما يضمن الخدمة المتزامنة.

في الممارسة العملية، يستخدم الخادم مجموعة مؤشرات ترابط ذات عدد محدود من مؤشرات الترابط، 50 على سبيل المثال. ثم يُطلب من العميل رقم 51 الانتظار.

10.4. تبادل المعلومات بين الخيوط

في الأمثلة السابقة، تم تهيئة مؤشر الترابط على النحو التالي:

Thread t=new Thread(Run);

حيث كانت Run طريقة ذات التوقيع التالي:

void Run();

من الممكن أيضًا استخدام التوقيع التالي:

void Run(object obj);

وهذا يسمح بنقل المعلومات إلى الخيط الذي تم تشغيله. على سبيل المثال،

t.Start(obj1);

سيطلق t الذي سيقوم بعد ذلك بتنفيذ Run المرتبط به حسب التصميم، ويمرر له المعلمة الفعالة obj1. إليك مثال على ذلك:


using System;
using System.Threading;

namespace Chap8 {
    class Program4 {
        public static void Main() {
             // init Current thread
            Thread main = Thread.CurrentThread;
             // name the Thread
            main.Name = "Main";
 
             // creation of execution threads
            Thread[] tâches = new Thread[5];
            Data[] data = new Data[5];
            for (int i = 0; i < tâches.Length; i++) {
                 // create thread i
                tâches[i] = new Thread(Sleep);
                 // set the thread name
                tâches[i].Name = i.ToString();
                 // start execution of thread i
                tâches[i].Start(data[i] = new Data { Début = DateTime.Now, Durée = i+1 });
            }
             // we wait for all threads
            for (int i = 0; i < tâches.Length; i++) {
                 // wait for thread i to finish execution
                tâches[i].Join();
                 // result display
                Console.WriteLine("Thread {0} terminé : début {1:hh:mm:ss}, durée programmée {2} s, fin {3:hh:mm:ss}, durée effective {4}",
                    tâches[i].Name,data[i].Début,data[i].Durée,data[i].Fin,(data[i].Fin-data[i].Début));
            }        
             // end of hand
            Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
        }
 
        public static void Sleep(object infos) {
             // parameter is retrieved
            Data data = (Data)infos;
             // sleep mode for Duration
            Thread.Sleep(data.Durée*1000);
             // end of execution
            data.Fin = DateTime.Now;
        }
    }
 
    internal class Data {
         // miscellaneous information
        public DateTime Début { get; set; }
        public int Durée { get; set; }
        public DateTime Fin { get; set; }
    }
}
  • الأسطر 45-50: معلومات من النوع [Data] تم تمريرها إلى الخيوط:
    • Start: وقت بدء تنفيذ الخيط - يتم تعيينه بواسطة خيط المشغل
    • المدة: مدة تنفيذ Sleep بالثواني بواسطة الخيط الذي تم تشغيله - يتم تعيينها بواسطة خيط المشغل
    • النهاية: وقت بدء تنفيذ الخيط - يتم تعيينه بواسطة الخيط الذي تم تشغيله
  • الأسطر 35-43: الطريقة Sleep التي تنفذها الخيوط لها التوقيع void Sleep(object obj). سيكون المعلمة الفعالة obj من النوع [Data] المحدد في السطر 45.
  • الأسطر 15-22: إنشاء 5 خيوط
  • السطر 17: يرتبط كل مؤشر ترابط بالطريقة Sleep في السطر 35
  • السطر 21: يتم تمرير كائن من النوع [Data] إلى Start الذي يقوم بتشغيل الخيط. في هذا الكائن، قمنا بتدوين وقت بدء تنفيذ الخيط والمدة بالثواني التي يجب أن يظل فيها في حالة نوم. يتم تخزين هذا الكائن في الجدول في السطر 14.
  • الأسطر 24-30: ينتظر مؤشر الترابط [Main] انتهاء جميع مؤشرات الترابط التي بدأها.
  • السطران 28-29: يسترد مؤشر الترابط [Main] كائن data[i] من مؤشر الترابط رقم i ويعرض محتوياته.
  • الأسطر 35-42: الطريقة Sleep التي تنفذها الخيوط
  • السطر 37: يتم استرداد المعلمة من النوع [Data]
  • السطر 39: يتم استخدام معلمة الحقل Duration لتعيين Sleep
  • السطر 41: يتم تهيئة حقل End الخاص بالمعلمة

والنتائج هي كما يلي:

1
2
3
4
5
6
Thread 0 terminé : début 11:18:50, durée programmée 1 s, fin 11:18:51, durée effective 00:00:01.0156250
Thread 1 terminé : début 11:18:50, durée programmée 2 s, fin 11:18:52, durée effective 00:00:02
Thread 2 terminé : début 11:18:50, durée programmée 3 s, fin 11:18:53, durée effective 00:00:03
Thread 3 terminé : début 11:18:50, durée programmée 4 s, fin 11:18:54, durée effective 00:00:04
Thread 4 terminé : début 11:18:50, durée programmée 5 s, fin 11:18:55, durée effective 00:00:05
Fin du thread Main à 11:18:55

يوضح هذا المثال أن خيطين يمكنهما تبادل المعلومات:

  • يمكن لسلسلة التشغيل المشغلة التحكم في تنفيذ سلسلة التشغيل التي تم تشغيلها من خلال تزويدها بالمعلومات
  • يمكن للخيط الذي تم تشغيله إرجاع النتائج إلى الخيط المشغل.

لكي يعرف الخيط الذي تم تشغيله متى تتوفر النتائج التي ينتظرها، يجب تنبيهه عند انتهاء الخيط الذي تم تشغيله. هنا، انتظر انتهاءه باستخدام Join. هناك طرق أخرى للقيام بنفس الشيء. سنلقي نظرة عليها لاحقًا.

10.5. التنافس على الوصول إلى الموارد المشتركة

10.5.1. الوصول المتزامن غير المتزامن

في الفقرة المتعلقة بتبادل المعلومات بين الخيوط، كان تبادل المعلومات يقتصر على خيطين فقط وفي أوقات محددة للغاية. كان هذا تمريرًا كلاسيكيًا للمعلمات. في حالات أخرى، يتم مشاركة المعلومات بين عدة خيوط، والتي قد ترغب في قراءتها أو تحديثها في نفس الوقت. وهذا يثير مشكلة سلامة المعلومات. لنفترض أن المعلومات المشتركة هي بنية S تحتوي على عناصر معلومات مختلفة I1، I2، ... In.

  • يبدأ مؤشر ترابط T1 في تحديث البنية S: فهو يعدل الحقل I1 ويتم مقاطعته قبل إكمال التحديث الكامل للبنية S
  • يقوم مؤشر ترابط T2 الذي يستعيد المعالج بقراءة البنية S لاتخاذ القرارات. ويقرأ بنية في حالة غير مستقرة: بعض الحقول محدثة، والبعض الآخر غير محدث.

نسمي هذه الحالة «الوصول إلى مورد مشترك»، وهو في هذه الحالة البنية S، وغالبًا ما يكون التعامل معها أمرًا صعبًا للغاية. لنأخذ المثال التالي لتوضيح المشكلات التي قد تنشأ:

  • سيقوم أحد التطبيقات بإنشاء n خيطًا، حيث يتم تمرير n كمعلمة
  • المورد المشترك هو عداد يجب زيادته بواسطة كل مؤشر تسلسل يتم إنشاؤه
  • في نهاية التطبيق، يتم عرض قيمة العداد. لذلك يجب أن نجد n.

البرنامج كما يلي:


using System;
using System.Threading;
 
namespace Chap8 {
    class Program {
 
         // class variables
         static int cptrThreads     = 0; // thread counter
 
         //hand
        public static void Main(string[] args) {
             // instructions for use
            const string syntaxe = "pg nbThreads";
            const int nbMaxThreads = 100;
 
             // verification no. of arguments
            if (args.Length != 1) {
                 // error
                Console.WriteLine(syntaxe);
                 // stop
                Environment.Exit(1);
            }
             // argument quality check
            int nbThreads = 0;
            bool erreur = false;
            try {
                nbThreads = int.Parse(args[0]);
                if (nbThreads < 1 || nbThreads > nbMaxThreads)
                    erreur = true;
            } catch {
                 // error
                erreur = true;
            }
             // mistake?
            if (erreur) {
                 // error
                Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et 100)");
                 // end
                Environment.Exit(2);
            }
             // thread creation and generation
            Thread[] threads = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                 // creation
                threads[i] = new Thread(Incrémente);
                 // naming
                threads[i].Name = "" + i;
                 // launch
                threads[i].Start();
            }//for
             // waiting for threads to finish
            for (int i = 0; i < nbThreads; i++) {
                threads[i].Join();
            }
             // counter display
            Console.WriteLine("Nombre de threads générés : " + cptrThreads);
        }
 
        public static void Incrémente() {
             // increases thread counter
             // meter reading
            int valeur = cptrThreads;
             // follow-up
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
             // waiting
            Thread.Sleep(1000);
             // counter incrementation
            cptrThreads = valeur + 1;
             // follow-up
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a écrit la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
        }
    }
}

لن نتطرق إلى جزء إنشاء الخيوط، الذي تمت تغطيته بالفعل. بدلاً من ذلك، دعونا نلقي نظرة على Increment، في السطر 59 الذي يستخدمه كل خيط لزيادة العداد الثابت cptrThreads في السطر 8.

  1. السطر 62: يتم قراءة العداد
  2. السطر 66: يتوقف الخيط لمدة ثانية واحدة. وبالتالي يفقد المعالج
  3. السطر 68: يتم زيادة العداد

الخطوة 2 موجودة فقط لإجبار الخيط على فقدان المعالج. سيتم منح المعالج لخيط آخر. في الممارسة العملية، ليس هناك ما يضمن أن الخيط لن يتم مقاطعته بين لحظة قراءة العداد ولحظة زيادته. حتى إذا كتبت cptrThreads++، مما يعطي انطباعًا بوجود تعليمة واحدة، فهناك خطر فقدان المعالج بين قراءة قيمة العداد وكتابة قيمته المضافة بـ 1. في الواقع، ستكون العملية عالية المستوى cptrThreads++ موضوعًا لعدة تعليمات أولية على مستوى المعالج. وبالتالي، فإن مرحلة السكون لمدة ثانية واحدة في الخطوة 2 موجودة فقط لتنظيم هذا الخطر.

النتائج التي تم الحصول عليها باستخدام 5 خيوط هي كما يلي:

A 12:00:56, le thread 3  a lu la valeur du compteur : 0
A 12:00:56, le thread 2  a lu la valeur du compteur : 0
A 12:00:56, le thread 1  a lu la valeur du compteur : 0
A 12:00:56, le thread 0  a lu la valeur du compteur : 0
A 12:00:56, le thread 4  a lu la valeur du compteur : 0
A 12:00:57, le thread 3  a écrit la valeur du compteur : 1
A 12:00:57, le thread 2  a écrit la valeur du compteur : 1
A 12:00:57, le thread 1  a écrit la valeur du compteur : 1
A 12:00:57, le thread 0  a écrit la valeur du compteur : 1
A 12:00:57, le thread 4  a écrit la valeur du compteur : 1
Nombre de threads générés : 1

بقراءة هذه النتائج، من السهل معرفة ما يحدث:

  • السطر 1: يقرأ الخيط الأول العداد. ويجد 0. يتوقف لمدة ثانية واحدة ويفقد المعالج
  • السطر 2: يسيطر مؤشر ترابط ثانٍ على المعالج ويقرأ أيضًا قيمة العداد. لا تزال القيمة عند 0، حيث لم يقم مؤشر الترابط السابق بزيادتها بعد. يتوقف هو أيضًا لمدة ثانية واحدة ويفقد المعالج.
  • الأسطر 1-5: في غضون ثانية واحدة، يتوفر الوقت لجميع الخيوط الخمسة لتمر وقراءة القيمة 0.
  • الأسطر 6-10: عندما تستيقظ الخيوط واحدًا تلو الآخر، ستزيد القيمة 0 التي قرأتها وتكتب القيمة 1 في العداد، كما يؤكد ذلك البرنامج الرئيسي (Main) في السطر 11.

ما المشكلة؟ قرأ الخيط الثاني القيمة الخاطئة لأن الخيط الأول قد تمت مقاطعته قبل إكمال مهمته المتمثلة في تحديث العداد في النافذة. وهذا يقودنا إلى مفهوم الموارد الحرجة والأقسام الحرجة في البرنامج:

  • المورد الحرج هو مورد لا يمكن أن يحتفظ به سوى خيط واحد في كل مرة. هنا، المورد الحرج هو العداد.
  • القسم الحرج في البرنامج هو سلسلة من التعليمات في مسار تنفيذ الخيط، يقوم خلالها الخيط بالوصول إلى مورد حرج. ويجب التأكد من أن هذا الخيط هو الخيط الوحيد الذي يصل إلى المورد خلال هذا القسم الحرج.

في مثالنا، الجزء الحرج هو الكود الموجود بين قراءة العداد وكتابة قيمته الجديدة:


            // lecture compteur
            int valeur = cptrThreads;
            // attente
            Thread.Sleep(1000);
            // incrémentation compteur
cptrThreads = valeur + 1;

لتنفيذ هذا الرمز، يجب ضمان أن يكون الخيط بمفرده. يمكن مقاطعته، ولكن أثناء هذه المقاطعة، يجب ألا يتمكن خيط آخر من تنفيذ نفس الرمز. توفر منصة .NET أدوات متنوعة لضمان الدخول الفردي إلى الأقسام الحرجة من الرمز. دعونا نلقي نظرة على بعضها.

10.5.2. جملة lock

يُستخدم جملة lock لتعريف قسم حرج على النحو التالي:

lock(obj){section critique}

يجب أن يكون obj مرجع كائن مرئي لجميع الخيوط التي تعمل في القسم الحرج. يضمن القفل أن خيطًا واحدًا فقط في كل مرة سيقوم بتنفيذ القسم الحرج. تمت إعادة كتابة المثال السابق على النحو التالي:


using System;
using System.Threading;
 
namespace Chap8 {
    class Program2 {
 
         // class variables
         static int cptrThreads     = 0; // thread counter
         static object synchro = new object(); // synchronization object
 
         //hand
        public static void Main(string[] args) {
    ...
             // waiting for threads to finish
            Thread.CurrentThread.Name = "Main";
            for (int i = nbThreads - 1; i >= 0; i--) {
                Console.WriteLine("A {0:hh:mm:ss}, le thread {1} attend la fin du thread {2}", DateTime.Now, Thread.CurrentThread.Name, threads[i].Name);
                threads[i].Join();
                Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a été prévenu de la fin du thread {2}", DateTime.Now, Thread.CurrentThread.Name, threads[i].Name);
            }
             // counter display
            Console.WriteLine("Nombre de threads générés : " + cptrThreads);
        }
 
        public static void Incrémente() {
             // increases thread counter
             // exclusive access to the meter is required
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  attend l'autorisation d'entrer dans la section critique", DateTime.Now, Thread.CurrentThread.Name);
            lock (synchro) {
                 // meter reading
                int valeur = cptrThreads;
                 // follow-up
                Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
                 // waiting
                Thread.Sleep(1000);
                 // counter incrementation
                cptrThreads = valeur + 1;
                 // follow-up
                Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a écrit la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
            }
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a quitté la section critique", DateTime.Now, Thread.CurrentThread.Name);
        }
    }
}
  • السطر 9: synchro هو الكائن الذي يقوم بمزامنة جميع الخيوط.
  • الأسطر 16-23: تنتظر الطريقة [Main] الخيوط بترتيب عكسي لإنشائها.
  • الأسطر 29-40: تم تأطير الجزء الحرج من الطريقة Increment بواسطة القفل.

النتائج التي تم الحصول عليها باستخدام 3 خيوط هي كما يلي:

A 09:37:09, le thread 0 attend l'autorisation d'entrer dans la section critique
A 09:37:09, le thread 0 a lu la valeur du compteur : 0
A 09:37:09, le thread 1 attend l'autorisation d'entrer dans la section critique
A 09:37:09, le thread 2 attend l'autorisation d'entrer dans la section critique
A 09:37:09, le thread Main attend la fin du thread 2
A 09:37:10, le thread 0 a écrit la valeur du compteur : 1
A 09:37:10, le thread 1 a lu la valeur du compteur : 1
A 09:37:10, le thread 0 a quitté la section critique
A 09:37:11, le thread 1 a écrit la valeur du compteur : 2
A 09:37:11, le thread 1 a quitté la section critique
A 09:37:11, le thread 2 a lu la valeur du compteur : 2
A 09:37:12, le thread 2 a écrit la valeur du compteur : 3
A 09:37:12, le thread 2 a quitté la section critique
A 09:37:12, le thread Main a été prévenu de la fin du thread 2
A 09:37:12, le thread Main attend la fin du thread 1
A 09:37:12, le thread Main a été prévenu de la fin du thread 1
A 09:37:12, le thread Main attend la fin du thread 0
A 09:37:12, le thread Main a été prévenu de la fin du thread 0
Nombre de threads générés : 3
  • الخيط 0 هو أول خيط يدخل القسم الحرج: الأسطر 1 و 2 و 6 و 8
  • سيتم حظر الخيطين الآخرين حتى يخرج الخيط 0 من القسم الحرج: السطور 3 و 4
  • يأتي الخيط 1 بعد ذلك: الأسطر 7 و9 و10
  • يأتي الخيط 2 بعد ذلك: الأسطر 11 و 12 و 13
  • السطر 14: يتم تحذير الخيط الرئيسي الذي ينتظر انتهاء الخيط 2
  • السطر 15: الخيط الرئيسي ينتظر الآن انتهاء الخيط 1. وقد انتهى هذا الخيط بالفعل. يتم إخطار الخيط الرئيسي على الفور، السطر 16.
  • السطران 17-18: تحدث نفس العملية مع الخيط 0
  • السطر 19: عدد الخيوط صحيح

10.5.3. فئة Mutex

يمكن أيضًا استخدام فئة System.Threading.Mutex لتحديد الأقسام الحرجة. وهي تختلف عن القفل من حيث الرؤية:

  • تقوم عبارة lock بمزامنة الخيوط في نفس التطبيق
  • تسمح لك فئة Mutex بمزامنة الخيوط من تطبيقات مختلفة.

سنستخدم المنشئ والطرق التالية:

public Mutex()
ينشئ Mutex M
public bool WaitOne()
يطلب مؤشر الترابط T1 الذي ينفذ M.WaitOne() خاصية كائن التزامن M. إذا لم يكن مؤشر الترابط M محتفظًا به أي مؤشر ترابط (كما كان في البداية)، يتم "منحه" لمؤشر الترابط T1 الذي طلبه. إذا قام مؤخراً مؤشر ترابط T2 بتنفيذ نفس العملية، فسيتم حظره. وذلك لأن Mutex لا يمكن أن ينتمي إلا إلى مؤشر ترابط واحد. وسيتم إلغاء قفله عندما يقوم مؤشر الترابط T1 بتحرير Mutex M الذي يحتفظ به. وبالتالي، يمكن حظر عدة مؤشرات ترابط أثناء انتظار Mutex M.
public void ReleaseMutex()
يتخلى الخيط T1 الذي يقوم بتنفيذ M.ReleaseMutex() عن ملكية Mutex M. عندما يفقد الخيط T1 المعالج، يمكن للنظام منحه لأحد الخيوط التي تنتظر Mutex M. سيحصل عليه خيط واحد فقط بالترتيب، بينما ستظل الخيوط الأخرى التي تنتظر M محجوبة

يدير Mutex M الوصول إلى المورد المشترك R. يطلب الخيط المورد R بواسطة M.WaitOne() ويقوم بإطلاقه بواسطة M.ReleaseMutex(). يعد الجزء الحرج من الكود الذي يجب تنفيذه بواسطة خيط واحد فقط في كل مرة موردًا مشتركًا. يمكن مزامنة تنفيذ الجزء الحرج على النحو التالي:

M.WaitOne();
// le thread est seul à entrer ici
// section critique
....
M.ReleaseMutex();

حيث M هو كائن Mutex. لا تنسَ تحرير Mutex الذي أصبح عديم الفائدة حتى يتمكن مؤشر ترابط آخر من دخول القسم الحرج، وإلا فإن مؤشرات الترابط التي تنتظر Mutex الذي لم يتم تحريره لن تتمكن أبدًا من الوصول إلى المعالج.

إذا طبقنا ما رأيناه للتو على المثال السابق، فسيصبح تطبيقنا كما يلي:


using System;
using System.Threading;
 
namespace Chap8 {
    class Program3 {
 
         // class variables
         static int cptrThreads     = 0; // thread counter
         static Mutex synchro = new Mutex(); // synchronization object
 
         //hand
        public static void Main(string[] args) {
    ...
        }
 
        public static void Incrémente() {
....
            synchro.WaitOne();
            try {
...
            } finally {
...
                synchro.ReleaseMutex();
            }
        }
    }
}
  • السطر 9: أصبح كائن تزامن الخيط الآن Mutex.
  • السطر 18: بداية القسم الحرج - يجب أن يدخل خيط واحد فقط. نقوم بالحظر حتى يصبح مزامنة Mutex متاحًا.
  • السطر 33: نظرًا لأن Mutex يجب أن يتم تحريره دائمًا، سواء حدث استثناء أم لا، فإننا ندير القسم الحرج باستخدام try / finally لتحرير Mutex في finally.
  • السطر 23: يتم تحرير Mutex بمجرد اجتياز القسم الحرج.

النتائج هي نفسها كما في السابق.

10.5.4. فئة AutoResetEvent

كائن AutoResetEvent هو حاجز لا يسمح بمرور سوى مؤشر ترابط واحد في كل مرة، تمامًا مثل الأداتين السابقتين lock و Mutex. ونقوم بإنشاء كائن AutoResetEvent على النحو التالي:

AutoResetEvent barrière=new AutoresetEvent(bool état);

تشير الحالة المنطقية إلى ما إذا كان الحاجز مغلقًا (false) أم مفتوحًا (true). سيشير الخيط الذي يرغب في عبور الحاجز إلى ذلك على النحو التالي:

barrière.WaitOne();
  • إذا كان الحاجز مفتوحًا، يمر الخيط ويُغلق الحاجز من خلفه. إذا كان هناك عدة خيوط في انتظار، يمكننا التأكد من أن خيطًا واحدًا فقط سيمر.
  • إذا كان الحاجز مغلقًا، يتم حظر الخيط. وسيقوم خيط آخر بفتحه عندما يحين الوقت المناسب. ويعتمد هذا الوقت كليًا على المشكلة التي يتم معالجتها. وسيتم فتح الحاجز بواسطة العملية:
barrière.Set(); 

قد يحدث أن يرغب مؤشر ترابط في إغلاق حاجز. ويمكنه القيام بذلك عن طريق:

barrière.Reset(); 

إذا استبدلنا، في المثال السابق، الكائن Mutex بكائن من نوع AutoResetEvent، يصبح الكود كما يلي:


using System;
using System.Threading;
 
namespace Chap8 {
    class Program4 {
 
         // class variables
         static int cptrThreads     = 0; // thread counter
         static EventWaitHandle synchro = new AutoResetEvent(false); // synchronization object
 
         //hand
        public static void Main(string[] args) {
....
             // we open the critical section barrier
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1} ouvre la barrière de la section critique", DateTime.Now, Thread.CurrentThread.Name);
            synchro.Set();
             // waiting for threads to finish
...
             // counter display
            Console.WriteLine("Nombre de threads générés : " + cptrThreads);
        }
 
        public static void Incrémente() {
             // increases thread counter
             // exclusive access to the meter is required
...
            synchro.WaitOne();
            try {
...
            } finally {
                 // release the resource
...
                synchro.Set();
            }
        }
    }
}
  • السطر 9: يتم إنشاء الحاجز مغلقًا. سيتم فتحه بواسطة السطر 16 في Main.
  • السطر 27: يطلب الخيط المسؤول عن زيادة عداد الخيوط الإذن للدخول إلى القسم الحرج. ستتراكم الخيوط المختلفة أمام الحاجز المغلق. عندما يفتحه Main، سيمر أحد الخيوط المنتظرة.
  • السطر 33: عندما ينتهي من عمله، يعيد فتح البوابة، مما يسمح لخيط آخر بالدخول.

النتائج مشابهة للنتائج السابقة.

10.5.5. فئة Interlocked

تتيح فئة Interlocked جعل مجموعة العمليات متجانسة. ضمن مجموعة العمليات المتجانسة، إما أن يتم تنفيذ جميع العمليات بواسطة الخيط الذي يشغل المجموعة، أو لا يتم تنفيذ أي منها على الإطلاق. لا تبقى في حالة يتم فيها تنفيذ بعض العمليات دون الأخرى. تم تصميم كائنات التزامن lock و Mutex و AutoResetEvent لجعل مجموعة من العمليات متجانسة. ويتحقق ذلك عن طريق حظر الخيوط. تتيح لك فئة Interlocked تجنب حظر الخيوط للعمليات البسيطة ولكن المتكررة. توفر فئة Interlocked الطرق الثابتة التالية:

Image

تحتوي الطريقة Incrementally على التوقيع التالي:

public static int Increment(ref int location);

تقوم هذه الطريقة بزيادة قيمة الإيجار. ويضمن أن تكون هذه العملية متجانسة.

يمكن أن يكون برنامج عد الخيوط لدينا كما يلي:


using System;
using System.Threading;
 
namespace Chap8 {
    class Program5 {
 
         // class variables
         static int cptrThreads     = 0; // thread counter
 
         //hand
        public static void Main(string[] args) {
...
        }
 
        public static void Incrémente() {
             // increments the thread counter
            Interlocked.Increment(ref cptrThreads);
        }
    }
}
  • السطر 17: يتم زيادة عداد مؤشرات الترابط بشكل متزامن.

10.6. التنافس على الوصول إلى موارد مشتركة متعددة

10.6.1. مثال

في الأمثلة السابقة، كان هناك مورد واحد مشترك بين الخيوط المختلفة. يمكن أن تصبح الحالة أكثر تعقيدًا إذا كان هناك عدة موارد وكانت تعتمد على بعضها البعض. يمكن أن يؤدي هذا إلى حالة تشابك. هذه الحالة، المعروفة أيضًا باسم حالة التعطل، تحدث عندما ينتظر خيطان بعضهما البعض. ضع في اعتبارك الإجراءات التالية التي تتبع بعضها البعض زمنيًا:

  • يحصل الخيط T1 على ملكية Mutex M1 للوصول إلى المورد المشترك R1
  • يحصل مؤشر الترابط T2 على ملكية Mutex M2 للوصول إلى مورد مشترك R2
  • يطلب الخيط T1 Mutex M2. يتم حظره.
  • يطلب الخيط T2 Mutex M1. يتم حظره.

هنا، ينتظر الخيطان T1 و T2 بعضهما البعض. تحدث هذه الحالة عندما يحتاج الخيطان إلى موردين مشتركين، المورد R1 الذي يتحكم فيه Mutex M1 والمورد R2 الذي يتحكم فيه Mutex M2. أحد الحلول الممكنة هو طلب كلا الموردين في نفس الوقت، باستخدام Mutex M واحد. لكن هذا ليس ممكنًا دائمًا، إذا كان الأمر يتطلب، على سبيل المثال، تعبئة مورد مكلف تستغرق وقتًا طويلاً. حل آخر هو أن يقوم الخيط الذي يمتلك M1 ولا يمكنه الحصول على M2، بتحرير M1 لتجنب التداخل.

  1. لدينا مصفوفة تقوم فيها بعض الخيوط بإيداع البيانات (الكتاب) وتقوم أخرى بقراءتها (القراء).
  2. الكتاب متساوون ولكنهم حصريون: يمكن لكتاب واحد فقط في كل مرة إدخال البيانات في الجدول.
  3. القراء متساوون ولكنهم حصريون: يمكن لقارئ واحد فقط في كل مرة قراءة البيانات المخزنة في الجدول.
  4. لا يمكن للقارئ قراءة البيانات الموجودة في الجدول إلا بعد أن يقوم كاتب بإيداع البيانات فيه، ولا يمكن للكاتب إيداع بيانات جديدة في الجدول إلا بعد أن يقرأ القارئ البيانات الموجودة فيه.

يمكن التمييز بين موردين مشتركين:

  • لوحة الكتابة: لا يمكن الوصول إليها إلا لكاتب واحد في كل مرة.
  • لوحة العرض للقراءة فقط: لا يمكن الوصول إليها إلا لقارئ واحد في كل مرة.

وترتيب استخدام هذه الموارد:

  • يجب أن يأتي القارئ دائمًا بعد الكاتب.
  • يجب أن يأتي الكاتب دائمًا بعد القارئ، باستثناء المرة الأولى.

يمكن التحكم في الوصول إلى هذين الموردين باستخدام حاجزين من نوع AutoResetEvent :

  • سيتحكم الحاجز peutEcrire في وصول الكُتّاب إلى اللوحة.
  • سيتحكم الحاجز peutLire في وصول القراء إلى اللوحة.
  • سيتم إنشاء الحاجز peutEcrire مفتوحًا في البداية، مما يسمح للكاتب الأول بالمرور ويمنع جميع الكتّاب الآخرين.
  • سيتم إنشاء الحاجز peutLire مغلقًا في البداية، مما يمنع جميع القراء.
  • عندما ينتهي الكاتب من عمله، يفتح بوابة peutLire للسماح للقارئ بالدخول.
  • عندما ينتهي القارئ من عمله، يفتح بوابة peutEcrire للسماح للكاتب بالدخول.

البرنامج الذي يوضح هذه المزامنة المدفوعة بالأحداث هو كما يلي:


using System;
using System.Threading;
 
namespace Chap8 {
    class Program {
         // use of reader and writer threads
         // illustrates the use of synchronization events
 
 
         // class variables
         static int[] data = new int[3    ]; // resource shared between reader and writer threads
         static Random objRandom = new Random(DateTime.Now.Second    ); // a random number generator
         static AutoResetEvent peutLir    e; // indicates that the contents of data can be read
         static AutoResetEvent peutEcrir    e; // indicates that you can write the contents of data
 
         //hand
        public static void Main(string[] args) {
 
             // number of threads to generate
            const int nbThreads = 2;
 
             // flag initialization
             peutLire = new AutoResetEvent(f    als e); // cannot be read yet
             peutEcrire = new AutoResetEvent(    tru e); // we can already write
 
             // creation of reader threads
            Thread[] lecteurs = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                 // creation
                lecteurs[i] = new Thread(Lire);
                lecteurs[i].Name = "L" + i.ToString();
                 // launch
                lecteurs[i].Start();
            }
 
             // creating writer threads
            Thread[] écrivains = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                 // creation
                écrivains[i] = new Thread(Ecrire);
                écrivains[i].Name = "E" + i.ToString();
                 // launch
                écrivains[i].Start();
            }
 
             //end of hand
            Console.WriteLine("Fin de Main...");
        }
 
         // read the contents of the table
        public static void Lire() {
...
        }
 
         // write in the table
        public static void Ecrire() {
....
        }
    }
}
  • السطر 11: بيانات الجدول هي المورد المشترك بين مؤشرات ترابط القارئ والكاتب. يتم مشاركتها للقراءة بواسطة مؤشرات ترابط القارئ وللكتابة بواسطة مؤشرات ترابط الكاتب.
  • السطر 13: يُستخدم الكائن «peutLire» لإعلام خيوط القراءة بأنها يمكنها قراءة بيانات المصفوفة. ويتم تعيين قيمته إلى «true» بواسطة خيط الكتابة الذي قام بملء بيانات الجدول. ويتم تهيئته بقيمة «false» في السطر 23. ويجب على خيط الكتابة ملء المصفوفة أولاً قبل تمرير الحدث «peutLire» إلى «real».
  • السطر 14: يُستخدم الكائن peutEcrire لتنبيه خيوط الكاتب بأن بإمكانها الكتابة إلى البيانات. يتم تعيينه إلى true بواسطة خيط القارئ الذي استخدم بيانات المصفوفة بالكامل. يتم تهيئته إلى true، السطر 24. بيانات الجدول متاحة للكتابة.
  • الأسطر 27-34: إنشاء وتشغيل خيوط القراءة
  • الأسطر 37-44: إنشاء وتشغيل خيوط الكتابة

الطريقة Read التي تنفذها خيوط القراءة هي كما يلي:


public static void Lire() {
             // follow-up
            Console.WriteLine("Méthode [Lire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
             // we have to wait for reading authorization
            peutLire.WaitOne();
             // table reading
            for (int i = 0; i < data.Length; i++) {
                 //wait 1 s
                Thread.Sleep(1000);
                 // display
                Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
            }
             // we can write
            peutEcrire.Set();
             // follow-up
            Console.WriteLine("Méthode [Lire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
        }
  • السطر 5: ننتظر حتى يرسل مؤشر ترابط الكاتب إشارة تفيد بأن المصفوفة قد تم ملؤها. عند استلام هذه الإشارة، يمكن لمؤشر ترابط قارئ واحد فقط من بين مؤشرات الترابط التي تنتظر هذه الإشارة أن يمر.
  • الأسطر 7-12: بيانات عملية الجدول مع وجود Sleep في المنتصف لإجبار الخيط على فقدان المعالج.
  • السطر 14: يُعلم خيوط الكتابة بأن المصفوفة قد تمت قراءتها ويمكن إعادة ملؤها.

تتمثل طريقة "الكتابة" التي تنفذها خيوط الكاتب فيما يلي:


public static void Ecrire() {
             // follow-up
            Console.WriteLine("Méthode [Ecrire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
             // we have to wait for write authorization
            peutEcrire.WaitOne();
             // writing table
            for (int i = 0; i < data.Length; i++) {
                 //wait 1 s
                Thread.Sleep(1000);
                 // display
                data[i] = objRandom.Next(0, 1000);
                Console.WriteLine("{0:hh:mm:ss} : L'écrivain {1} a écrit le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
            }
            // on peut lire
            peutLire.Set();
             // follow-up
            Console.WriteLine("Méthode [Ecrire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
        }
  • السطر 5: ننتظر حتى يرسل مؤشر ترابط القارئ إشارة تفيد بأن المصفوفة قد تمت قراءتها. عند استلام هذه الإشارة، يمكن لواحد فقط من مؤشرات ترابط الكاتب التي تنتظر هذه الإشارة المرور.
  • الأسطر 7-13: بيانات عملية الجدول مع وجود Sleep في المنتصف لإجبار الخيط على فقدان المعالج.
  • السطر 15: يُعلم خيوط القراءة بأن المصفوفة قد تم ملؤها ويمكن قراءتها مرة أخرى.

يؤدي التنفيذ إلى النتائج التالية:

Méthode [Lire] démarrée par le thread n° L0
Méthode [Lire] démarrée par le thread n° L1
Méthode [Ecrire] démarrée par le thread n° E0
Méthode [Ecrire] démarrée par le thread n° E1
Fin de Main...
02:29:18 : L'écrivain E0 a écrit le nombre 607
02:29:19 : L'écrivain E0 a écrit le nombre 805
02:29:20 : L'écrivain E0 a écrit le nombre 650
Méthode [Ecrire] terminée par le thread n° E0
02:29:21 : Le lecteur L0 a lu le nombre 607
02:29:22 : Le lecteur L0 a lu le nombre 805
02:29:23 : Le lecteur L0 a lu le nombre 650
Méthode [Lire] terminée par le thread n° L0
02:29:24 : L'écrivain E1 a écrit le nombre 186
02:29:25 : L'écrivain E1 a écrit le nombre 881
02:29:26 : L'écrivain E1 a écrit le nombre 415
Méthode [Ecrire] terminée par le thread n° E1
02:29:27 : Le lecteur L1 a lu le nombre 186
02:29:28 : Le lecteur L1 a lu le nombre 881
02:29:29 : Le lecteur L1 a lu le nombre 415
Méthode [Lire] terminée par le thread n° L1

تجدر الإشارة إلى النقاط التالية:

  • لا يوجد سوى محرك أقراص واحد في كل مرة، على الرغم من أنه يفقد المعالج في القسم الحرج "قراءة"
  • لا يوجد سوى كاتب واحد في كل مرة، على الرغم من أنه يفقد المعالج في القسم المراجعة Write
  • لا يقرأ القارئ إلا عندما يكون هناك شيء لقراءته في الجدول
  • لا يكتب الكاتب حتى يتم قراءة الصورة بالكامل

10.6.2. فئة Monitor

في المثال السابق:

  • هناك موردان مشتركان يجب إدارتهما
  • بالنسبة لمورد معين، تكون الخيوط متساوية.

عندما يتم حظر خيوط الكاتب في peutEcrire.WaitOne، يتم إلغاء قفل إحداها، أي واحدة منها، بواسطة العملية peutEcrire.Set. إذا كانت العملية السابقة تنطوي على فتح البوابة لكاتب معين، فإن الأمور تصبح أكثر تعقيدًا.

التشبيه هو بمؤسسة تخدم الجمهور عند الكاونترات، حيث كل كاونتر متخصص. عندما يصل العملاء، يأخذون تذكرة من آلة إصدار التذاكر للكاونتر X ثم يجلسون. كل تذكرة مرقمة، ويتم استدعاء العملاء برقمهم عبر مكبر الصوت. أثناء الانتظار، يمكن للعملاء أن يفعلوا ما يشاؤون. يمكنهم القراءة أو الغفوة. في كل مرة، يستيقظ العميل على صوت مكبر الصوت الذي يعلن أن الرقم Y قد تم استدعاؤه إلى الكاونتر X. إذا كان هو المقصود، ينهض العميل ويتوجه إلى الكاونتر X، وإلا فإنه يواصل ما كان يفعله.

يمكننا أن نفعل ذلك هنا بطريقة مماثلة. خذ الكتّاب على سبيل المثال:

ينتظر العديد من الكتاب عند نفس الشباك
يتم حظر سلاسلهم
يتم تحرير النافذة ويتم استدعاء رقم الكاتب التالي
يخبر الخيط الذي كان يقرأ المصفوفة الكتّاب بأن المصفوفة متاحة. يقوم هو أو خيط آخر بتعيين خيط الكاتب لعبور الحاجز.
ينظر كل كاتب إلى رقمه، ولا يذهب إلى النافذة سوى من تم استدعاء رقمه
يذهب إلى النافذة. أما الباقون فيعودون إلى وضع الانتظار.
يتحقق كل مؤشر ترابط لمعرفة ما إذا كان هو المختار. إذا كان كذلك، فإنه يتجاوز الحاجز. إذا لم يكن كذلك، فإنه يعود إلى وضع الانتظار.

تُستخدم فئة Monitor لتنفيذ هذا السيناريو.

Image

نصف الآن بنية قياسية (نمط)، مقترحة في الفصل "التعدد الخيطي" من كتاب C# 3.0 المشار إليه في مقدمة هذا المستند، قادرة على حل مشاكل الحواجز ذات شروط الدخول.

  • أولاً، تصل الخيوط التي تتشارك مورداً (العداد، إلخ) إليه عبر كائن سنسميه رمزاً. لفتح البوابة المؤدية إلى العداد، يجب أن يكون لديك الرمز لفتحه، ولا يوجد سوى رمز واحد. لذلك، يجب أن تتبادل الخيوط الرمز فيما بينها.
object jeton=new object();
  • للوصول إلى العداد، تطلب الخيوط أولاً:
Monitor.Enter(jeton);

إذا كان الرمز متاحًا، يتم منحه للخيط الذي نفذ العملية السابقة، وإلا يتم تعليق الخيط في انتظار الرمز.

  • إذا كان الوصول إلى العداد غير مرتب، أي إذا كان الشخص الذي يدخل لا يهم، فإن العملية السابقة كافية. يذهب الخيط الذي يحمل الرمز إلى العداد. إذا كان الوصول مرتبًا، يتحقق الخيط الذي يحمل الرمز من أنه يستوفي شرط الذهاب إلى العداد:
while (! jeNeSuisPasCeluiQuiEstAttendu) {Monitor.Wait(jeton);}

إذا لم يكن الخيط هو الخيط المتوقع عند العداد، فإنه يتنازل عن دوره عن طريق إعادة الرمز المميز. ويدخل في حالة حظر. وسيتم إيقاظه بمجرد أن يصبح الرمز المميز متاحًا مرة أخرى. ثم سيتحقق مرة أخرى مما إذا كان يستوفي شرط الذهاب إلى العداد. لا يمكن إجراء العملية Monitor.Wait(token) التي تطلق الرمز المميز إلا إذا كان الخيط يمتلك الرمز المميز. إذا لم يكن الأمر كذلك، يتم إصدار استثناء.

  • يذهب الخيط الذي يتحقق من الشرط للذهاب إلى العداد إلى هناك:
  1. // عمل العداد
  2. ....

قبل مغادرة العداد، يجب أن يعيد الخيط رمزه، وإلا فإن الخيوط المحجوبة في انتظار ذلك ستظل محجوبة إلى أجل غير مسمى. هناك حالتان مختلفتان:

  • الحالة الأولى هي عندما يكون الخيط الذي يحمل الرمز هو نفسه الذي يُعلم الخيوط التي تنتظر الرمز بأنه أصبح متاحًا. وسيقوم بذلك على النحو التالي:
1
2
3
4
5
6
7
8
// travail au guichet
....
// modification condition d'accès au guichet
...
// réveil des threads en attente du jeton
Monitor.PulseAll(jeton);
// libération du jeton
Monitor.Exit(jeton);

في السطر 6، يتم إيقاظ الخيوط التي تنتظر الرمز. وهذا يعني أنها تصبح مؤهلة لتلقي الرمز. ولا يعني ذلك أنها تتلقاه على الفور. في السطر 8، يتم تحرير الرمز. ستتلقى جميع الخيوط المؤهلة الرمز بالترتيب، بشكل غير محدد. سيعطيهم هذا الفرصة للتحقق مرة أخرى مما إذا كانوا يستوفون شرط الوصول. قامت الخيط التي حررت الرمز بتعديل هذا الشرط في السطر 4 للسماح لخيط جديد بالدخول. تحتفظ الخيط الأولى التي تتحقق من هذا الشرط بالرمز وتنتقل إلى العداد بالترتيب.

  • الحالة الثانية هي عندما لا يكون الخيط الذي يحمل الرمز هو الذي يرسل إشارة إلى الخيوط التي تنتظر الرمز بأنه متاح. ومع ذلك، يجب عليه تحريره، لأن الخيط المسؤول عن إرسال هذه الإشارة يجب أن يكون حامل الرمز. وسيقوم بذلك باستخدام العملية:
Monitor.Exit(jeton);

أصبح الرمز متاحًا الآن، ولكن الخيوط التي تنتظره (التي قامت بعملية Wait(token)) لم يتم إخطارها. يتم إسناد هذه المهمة إلى خيط آخر، والذي سيقوم في مرحلة ما بتنفيذ كود مشابه لما يلي:

1
2
3
4
5
6
7
8
// acquisition jeton
Monitor.Enter(jeton);
// modification condition d'accès au guichet
....
// réveil des threads en attente du jeton
Monitor.PulseAll(jeton);
// libération du jeton
Monitor.Exit(jeton);

في النهاية، فإن البنية القياسية المقترحة في فصل "Threading" من كتاب C# 3.0 هي كما يلي:

  • define counter access token :
object jeton=new object();
  • طلب الوصول إلى العداد:
lock(jeton){
    while (! jeNeSuisPasCeluiQuiEstAttendu) 
        Monitor.Wait(jeton);
}
// passage au guichet
...
lock(jeton){...} 

يعادل

Monitor.Enter(jeton);
try{...} finally{Monitor.Exit(jeton);}

لاحظ أنه في هذا المخطط يتم تحرير الرمز على الفور، بمجرد تجاوز الحاجز. يمكن بعد ذلك لخيط آخر اختبار شرط الوصول. وبالتالي، فإن البنية السابقة تسمح لجميع الخيوط بالتحقق من شرط الوصول. إذا لم يكن هذا ما تريده، يمكنك كتابة:

lock(jeton){
    while (! jeNeSuisPasCeluiQuiEstAttendu) 
        Monitor.Wait(jeton);
    // passage au guichet
    ...
}

حيث يتم تحرير الرمز المميز فقط بعد المرور عبر الكاونتر.

  • تعديل شروط الوصول إلى العداد وإخطار الخيوط الأخرى
lock(jeton){
    // modifier la condition d'accès au guichet
    ...
    // en avertir les threads en attente du jeton
    Monitor.PulseAll(jeton);
}

فيما سبق، لا يمكن تعديل شرط الوصول إلا من قبل الخيط الذي يحمل الرمز المميز. يمكنك أيضًا كتابة:

    // modifier la condition d'accès au guichet
    ...
    // en avertir les threads en attente du jeton
    Monitor.PulseAll(jeton);
    // libérer le jeton
    Monitor.Exit(jeton);

إذا كان الخيط يمتلك الرمز بالفعل.

بناءً على هذه المعلومات، يمكننا إعادة كتابة تطبيق القراء/الكتاب، وتحديد ترتيب وصول القراء والكتاب إلى عداداتهم الخاصة. وفيما يلي الكود:


using System;
using System.Threading;
 
namespace Chap8 {
    class Program2 {
         // use of reader and writer threads
         // illustrates the use of synchronization events
 
 
         // class variables
         static int[] data = new int[3            ]; // resource shared between reader and writer threads
         static Random objRandom = new Random(DateTime.Now.Second    ); // a random number generator
         static object peutLire = new object(        ); // indicates that the contents of data can be read
         static object peutEcrire = new object(    ); // indicates that you can write the contents of data
         static bool lectureAutorisée = fals    e; // to authorize the reading of the table
         static bool écritureAutorisée = fals    e; // to authorize writing in the table
         static string[] ordreLectur    e; // sets the order of readers
         static string[] ordreEcritur    e; // sets the order for writers
         static int lecteurSuivant =     0; // indicates the next drive number
         static int écrivainSuivant =     0; // indicates the number of the following writer
 
         //hand
        public static void Main(string[] args) {
 
             // number of threads to generate
            const int nbThreads = 5;
 
             // creation of reader threads
            Thread[] lecteurs = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                 // creation
                lecteurs[i] = new Thread(Lire);
                lecteurs[i].Name = "L" + i.ToString();
                 // launch
                lecteurs[i].Start();
            }
 
             // create playback order
            ordreLecture = new string[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                ordreLecture[i] = lecteurs[nbThreads - i - 1].Name;
                Console.WriteLine("Le lecteur {0} est en position {1}", ordreLecture[i], i);
            }
 
             // creating writer threads
            Thread[] écrivains = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                 // creation
                écrivains[i] = new Thread(Ecrire);
                écrivains[i].Name = "E" + i.ToString();
                 // launch
                écrivains[i].Start();
            }
 
             // creation of writing order
            ordreEcriture = new string[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                ordreEcriture[i] = écrivains[i].Name;
                Console.WriteLine("L'écrivain {0} est en position {1}", ordreEcriture[i], i);
            }
 
             // write authorization
            lock (peutEcrire) {
               écritureAutorisée = true;
                Monitor.Pulse(peutEcrire);
            }
 
 
             //end of hand
            Console.WriteLine("Fin de Main...");
        }
 
         // read the contents of the table
        public static void Lire() {
...
        }
 
         // write in the table
        public static void Ecrire() {
...
        }
    }
}

يخضع الدخول إلى مكتب القراءة للشروط التالية:

  • السطر 13: الرمز المميز peutLire
  • السطر 15: القيمة المنطقية readingAuthorized
  • السطر 17: الجدول المرتب للقراء. يذهب القراء إلى مكتب القراءة حسب ترتيب هذا الجدول، الذي يحتوي على أسمائهم.
  • السطر 19: يشير lecteurSuivant إلى رقم القارئ التالي المصرح له بالذهاب إلى مكتب الاستقبال.

يخضع الوصول إلى مكتب الكتابة للشروط التالية:

  • السطر 14: الرمز المميز peutEcrire
  • السطر 16: القيمة المنطقية writingAuthorized
  • السطر 18: جدول الكُتّاب المرتب. يذهب الكُتّاب إلى مكتب الكتابة حسب ترتيب هذا الجدول الذي يحتوي على أسمائهم.
  • السطر 20: تشير "writerNext" إلى رقم الكاتب التالي المصرح له بالانتقال إلى الكاونتر.

العناصر الأخرى في الكود هي كما يلي:

  • السطور 29-36: إنشاء وتشغيل خيوط القارئ. سيتم حظرها جميعًا لأن القراءة غير مصرح بها (السطر 15).
  • الأسطر 39-43: سيكون ترتيب مرورها عبر العداد عكس ترتيب إنشائها.
  • الأسطر 46-53: إنشاء وتشغيل خيوط الكتابة. سيتم حظرها جميعًا لأن الكتابة غير مسموح بها (السطر 16).
  • الأسطر 56-60: سيكون ترتيب مرورها عبر العداد هو ترتيب إنشائها.
  • السطر 64: الكتابة مسموح بها
  • السطر 65: يتم تحذير الكُتّاب بأن شيئًا ما قد تغير.

الطريقة Read هي كما يلي:


        public static void Lire() {
             // follow-up
            Console.WriteLine("Méthode [Lire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
             // we have to wait for reading authorization
            lock (peutLire) {
                while (!lectureAutorisée || ordreLecture[lecteurSuivant] != Thread.CurrentThread.Name) {
                    Monitor.Wait(peutLire);
                }
                 // table reading
                for (int i = 0; i < data.Length; i++) {
                     //wait 1 s
                    Thread.Sleep(1000);
                     // display
                    Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
                }
                 // next reader
                lectureAutorisée = false;
                lecteurSuivant++;
                 // writers are warned that they can write
                lock (peutEcrire) {
                    écritureAutorisée = true;
                    Monitor.PulseAll(peutEcrire);
                }
 
                 // follow-up
                Console.WriteLine("Méthode [Lire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
            }
}
  • يتم التحكم في كل الوصول إلى الكاونتر بواسطة القفل في الأسطر 5-27. القارئ الذي يحصل على الرمز يحتفظ به طوال زيارته للكاونتر
  • الأسطر 6-8: يقوم القارئ الذي حصل على الرمز في السطر 5 بإطلاقه إذا لم يكن القراءة مسموحة أو إذا لم يكن دوره في المرور.
  • الأسطر 10-15: المرور عبر الكاونتر (عملية الجدول)
  • السطور 17-18: يغير الخيط شروط الوصول إلى مكتب القراءة. لاحظ أنه لا يزال يحتفظ برمز القراءة وأن هذه التعديلات لا تسمح للقارئ بالمرور بعد.
  • الأسطر 20-23: يغير الخيط شروط الوصول إلى مكتب الكتابة ويحذر جميع الكتّاب المنتظرين من حدوث تغيير ما.
  • السطر 27: ينتهي القفل، ويتم تحرير الرمز peutLire. يمكن لخيط القراءة الحصول عليه في السطر 5، لكنه لن يجتاز شرط الوصول، لأن القيمة المنطقية readingAuthorized هي false. بالإضافة إلى ذلك، تظل جميع الخيوط التي تنتظر peutLire في انتظار، لأن PulseAll(peutLire) لم يتم بعد.

الطريقة Write هي كما يلي:


        public static void Ecrire() {
             // follow-up
            Console.WriteLine("Méthode [Ecrire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
             // we have to wait for write authorization
            lock (peutEcrire) {
                while (!écritureAutorisée || ordreEcriture[écrivainSuivant] != Thread.CurrentThread.Name) {
                    Monitor.Wait(peutEcrire);
                }
                 // writing table
                for (int i = 0; i < data.Length; i++) {
                     //wait 1 s
                    Thread.Sleep(1000);
                     // display
                    data[i] = objRandom.Next(0, 1000);
                    Console.WriteLine("{0:hh:mm:ss} : L'écrivain {1} a écrit le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
                }
                 // next writer
                écritureAutorisée = false;
                écrivainSuivant++;
                 // readers waiting for the peutLire token are woken up
                lock (peutLire) {
                    lectureAutorisée = true;
                    Monitor.PulseAll(peutLire);
                }
                 // follow-up
                Console.WriteLine("Méthode [Ecrire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
            }
}
  • يتم التحكم في كل الوصول إلى مكتب الكتابة بواسطة القفل الأسطر 5-27. الكاتب الذي يحصل على الرمز يحتفظ به طوال فترة وجوده عند المكتب
  • الأسطر 6-8: الكاتب الذي حصل على الرمز في السطر 5 يطلقه إذا لم يُسمح بالكتابة أو إذا لم يحن دوره للمرور.
  • الأسطر 10-16: المرور عبر الكاونتر (عملية الجدول)
  • السطور 18-19: يغير الخيط شروط الوصول إلى مكتب الكتابة. لاحظ أنه لا يزال يمتلك رمز الكتابة وأن هذه التعديلات لا تسمح للكاتب بالمرور بعد.
  • الأسطر 21-24: يغير الخيط شروط الوصول إلى مكتب القراءة ويحذر جميع القراء المنتظرين من حدوث تغيير ما.
  • السطر 27: ينتهي القفل، ويتم تحرير الرمز peutEcrire. يمكن لخيط الكتابة الحصول عليه في السطر 5، لكنه لن يجتاز شرط الوصول، لأن القيمة المنطقية writingAuthorized هي false. بالإضافة إلى ذلك، تظل جميع الخيوط التي تنتظر peutEcrire في حالة انتظار حتى إجراء عملية جديدة PulseAll(peutEcrire).

فيما يلي مثال على التنفيذ:

Méthode [Lire] démarrée par le thread n° L0
Méthode [Lire] démarrée par le thread n° L2
Méthode [Lire] démarrée par le thread n° L1
Le lecteur L2 est en position 0
Le lecteur L1 est en position 1
Le lecteur L0 est en position 2
Méthode [Ecrire] démarrée par le thread n° E0
Méthode [Ecrire] démarrée par le thread n° E1
L'écrivain E0 est en position 0
L'écrivain E1 est en position 1
L'écrivain E2 est en position 2
Fin de Main...
Méthode [Ecrire] démarrée par le thread n° E2
12:09:05 : L'écrivain E0 a écrit le nombre 815
12:09:06 : L'écrivain E0 a écrit le nombre 990
12:09:07 : L'écrivain E0 a écrit le nombre 563
Méthode [Ecrire] terminée par le thread n° E0
12:09:08 : Le lecteur L2 a lu le nombre 815
12:09:09 : Le lecteur L2 a lu le nombre 990
12:09:10 : Le lecteur L2 a lu le nombre 563
Méthode [Lire] terminée par le thread n° L2
12:09:11 : L'écrivain E1 a écrit le nombre 411
12:09:12 : L'écrivain E1 a écrit le nombre 11
12:09:13 : L'écrivain E1 a écrit le nombre 54
Méthode [Ecrire] terminée par le thread n° E1
12:09:14 : Le lecteur L1 a lu le nombre 411
12:09:15 : Le lecteur L1 a lu le nombre 11
12:09:16 : Le lecteur L1 a lu le nombre 54
Méthode [Lire] terminée par le thread n° L1
12:09:17 : L'écrivain E2 a écrit le nombre 698
12:09:18 : L'écrivain E2 a écrit le nombre 448
12:09:19 : L'écrivain E2 a écrit le nombre 472
Méthode [Ecrire] terminée par le thread n° E2
12:09:20 : Le lecteur L0 a lu le nombre 698
12:09:21 : Le lecteur L0 a lu le nombre 448
12:09:22 : Le lecteur L0 a lu le nombre 472
Méthode [Lire] terminée par le thread n° L0

10.7. مجموعات الخيوط

حتى الآن، لإدارة:

  • كنا نقوم بإنشائها بواسطة Thread T=new Thread(...)
  • ثم نفذناها بواسطة T.Start()

لقد رأينا في فصل "قواعد البيانات" أنه مع بعض أنظمة إدارة قواعد البيانات (SGBD) كان من الممكن الحصول على مجموعات من الاتصالات المفتوحة:

  • يتم فتح n اتصال عند بدء تشغيل المجموعة
  • عندما يطلب مؤشر ترابط اتصالاً، يتم منحه أحد الاتصالات المفتوحة في المجموعة
  • عندما يغلق الخيط الاتصال، لا يتم إغلاقه بل يتم إرجاعه إلى المجموعة

استخدام مجموعة الاتصالات شفاف من حيث الكود. وتكمن الميزة في تحسين الأداء: فتح اتصال مكلف. هنا يمكن لـ 10 اتصالات مفتوحة تلبية مئات الطلبات.

يوجد نظام مشابه للخيوط:

  • يتم إنشاء عدد أدنى من الخيوط عند بدء تشغيل المجموعة. يتم تعيين قيمة min باستخدام ThreadPool.SetMinThreads(min1,min2). يمكن استخدام تجمع الخيوط لتنفيذ مهام غير متزامنة معطلة أو غير معطلة. المعلمة الأولى min1 تحدد عدد الخيوط المعطلة، والثانية min2 تحدد عدد الخيوط غير المتزامنة. يمكن الحصول على القيم الحالية لهاتين المتغيرتين بواسطة ThreadPool.GetMinThreads(out min1,out min2).
  • إذا لم يكن هذا العدد كافيًا، فسيقوم المجمع بإنشاء خيوط أخرى للاستجابة للطلبات حتى الحد الأقصى لعدد الخيوط. يتم تعيين قيمة max باستخدام ThreadPool.SetMaxThreads(max1,max2). كلا المعلمتين لهما نفس المعنى كما في SetMinThreads. يمكن الحصول على القيم الحالية لهاتين القيمتين بواسطة ThreadPool.GetMaxThreads(out max1,out max2). عند الوصول إلى max1 من الخيوط، سيتم وضع طلبات الخيوط للمهام الحاجزة في قائمة انتظار حتى يتوفر خيط حر في المجموعة.

يوفر تجمع الخيوط عددًا من المزايا:

  • كما هو الحال مع تجمع الاتصالات، نوفر وقت إنشاء الخيوط: يمكن لـ 10 خيوط خدمة مئات الطلبات.
  • نحن نؤمن التطبيق: من خلال تعيين عدد أقصى من الخيوط، نتجنب إرباك التطبيق بعدد كبير جدًا من الطلبات. سيتم وضع هذه الطلبات في قائمة انتظار الملفات.

لتعيين مهمة إلى مؤشر ترابط في المجموعة، استخدم إحدى الطريقتين التاليتين:

  1. ThreadPool.QueueWorkItem(WaitCallBack)
  2. ThreadPool.QueueWorkItem(WaitCallBack,object)

حيث WaitCallBack هي أي طريقة ذات التوقيع void WaitCallBack(object). تطلب الطريقة 1 من مؤشر الترابط تنفيذ الطريقة WaitCallBack دون تمرير معلمة. تقوم الطريقة 2 بنفس الشيء، ولكنها تمرر معلمة من النوع object إلى WaitCallBack.

يوضح البرنامج التالي هذه المفاهيم:


using System;
using System.Threading;
 
namespace Chap8 {
    class Program {
        public static void Main() {
             // init Current thread
            Thread main = Thread.CurrentThread;
            // name the Thread
            main.Name = "Main";
 
            // we use a thread pool
            int min1, min2;
            // set the minimum number of blocking threads
            ThreadPool.GetMinThreads(out min1, out min2);
            Console.WriteLine("Nombre minimum de tâches bloquantes dans le pool : {0}", min1);
            Console.WriteLine("Nombre minimum de tâches asynchrones dans le pool : {0}", min2);
            ThreadPool.SetMinThreads(3, min2);
            ThreadPool.GetMinThreads(out min1, out min2);
            Console.WriteLine("Nombre minimum de tâches bloquantes dans le pool après changement : {0}", min1);
            // set the maximum number of blocking threads
            int max1, max2;
            ThreadPool.GetMaxThreads(out max1, out max2);
            Console.WriteLine("Nombre maximum de tâches bloquantes dans le pool : {0}", max1);
            Console.WriteLine("Nombre maximum de tâches asynchrones dans le pool : {0}", max2);
            ThreadPool.SetMaxThreads(5, max2);
            ThreadPool.GetMaxThreads(out max1, out max2);
            Console.WriteLine("Nombre maximum de tâches bloquantes dans le pool après changement : {0}", max1);
            // 7 threads are executed
            for (int i = 0; i < 7; i++) {
                // start execution of thread i in a pool
                ThreadPool.QueueUserWorkItem(Sleep, new Data2 { Numéro = i.ToString(), Début = DateTime.Now, Durée = i + 10 });
            }
             // end of hand
            Console.Write("Tapez [entrée] pour terminer le thread {0} à {1:hh:mm:ss:FF}", main.Name, DateTime.Now);
             // waiting
            Console.ReadLine();
        }
 
        public static void Sleep(object infos) {
            // parameter is retrieved
            Data2 data = infos as Data2;
            Console.WriteLine("A {2:hh:mm:ss:FF}, le thread n° {0} va dormir pendant {1} seconde(s)", data.Numéro, data.Durée,DateTime.Now);
             // pool status
            int cpt1, cpt2;
            ThreadPool.GetAvailableThreads(out cpt1, out cpt2);
            Console.WriteLine("Nombre de threads pour tâches bloquantes disponibles dans le pool : {0}", cpt1);
            // sleep mode for Duration
            Thread.Sleep(data.Durée * 1000);
             // end of execution
            data.Fin = DateTime.Now;
            Console.WriteLine("A {3:hh:mm:ss:FF}, le thread n° {0} se termine. Il était programmé pour durer {1} seconde(s). Il a duré {2} seconde(s)", data.Numéro, data.Durée, data.Fin - data.Début,DateTime.Now);
        }
    }
 
    internal class Data2 {
         // miscellaneous information
        public string Numéro { get; set; }
        public DateTime Début { get; set; }
        public int Durée { get; set; }
        public DateTime Fin { get; set; }
    }
}
  • السطور 15-17: يتم طلب وعرض الحد الأدنى الحالي لعدد الخيوط في تجمع الخيوط
  • السطر 18: تغيير الحد الأدنى لعدد الخيوط للمهام الحاجبة إلى 2
  • الأسطر 19-21: يتم عرض الحد الأدنى الجديد
  • الأسطر 22-28: قم بنفس الشيء لتعيين الحد الأقصى لعدد الخيوط للمهام المعطلة: 5
  • الأسطر 30-33: يتم تنفيذ 7 مهام في مجموعة من 5 خيوط. يجب أن تحصل 5 مهام على خيط واحد، أول اثنتين بسرعة لأن خيطين متاحين دائمًا، والثلاث الأخرى بزمن انتظار يبلغ 0.5 ثانية. يجب أن تنتظر مهمتان حتى يتوفر خيط.
  • السطر 32: تنفذ المهام الأمر Sleep في الأسطر 40-54 عن طريق تمرير معلمة من النوع Data2 المحددة في الأسطر 56-62.
  • السطر 40: يتم تنفيذ الأسلوب Sleep بواسطة المهام
  • السطر 42: يسترد المعلمة التي تم تمريرها إلى Sleep.
  • السطر 43: تحدد المهمة هويتها على وحدة التحكم
  • الأسطر 45-47: تعرض عدد الخيوط المتاحة حاليًا. نريد أن نرى كيف يتطور ذلك.
  • السطر 49: تتوقف المهمة لبضع ثوانٍ (مهمة حجب).
  • السطر 52: عندما تستيقظ، نعرض بعض المعلومات حول حسابها.

النتائج هي كما يلي.

بالنسبة لعدد الخيوط الدنيا والقصوى في المجموعة:

1
2
3
4
5
6
Nombre minimum de tâches bloquantes dans le pool : 2
Nombre minimum de tâches asynchrones dans le pool : 2
Nombre minimum de tâches bloquantes dans le pool après changement : 3
Nombre maximum de tâches bloquantes dans le pool : 500
Nombre maximum de tâches asynchrones dans le pool : 1000
Nombre maximum de tâches bloquantes dans le pool après changement : 5

لتشغيل الخيوط السبعة:

A 03:07:37:04, le thread n° 0 va dormir pendant 10 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 3
A 03:07:37:04, le thread n° 2 va dormir pendant 12 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 2
A 03:07:37:04, le thread n° 1 va dormir pendant 11 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 2
A 03:07:38:04, le thread n° 3 va dormir pendant 13 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 1
A 03:07:38:54, le thread n° 4 va dormir pendant 14 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
A 03:07:47:04, le thread n° 0 se termine. Il était programmé pour durer 10 seconde(s). Il a duré 00:00:10 seconde(s)
A 03:07:47:04, le thread n° 5 va dormir pendant 15 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
A 03:07:48:04, le thread n° 1 se termine. Il était programmé pour durer 11 seconde(s). Il a duré 00:00:11 seconde(s)
A 03:07:48:04, le thread n° 6 va dormir pendant 16 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
A 03:07:49:04, le thread n° 2 se termine. Il était programmé pour durer 12 seconde(s). Il a duré 00:00:12 seconde(s)
A 03:07:51:04, le thread n° 3 se termine. Il était programmé pour durer 13 seconde(s). Il a duré 00:00:14 seconde(s)
A 03:07:52:54, le thread n° 4 se termine. Il était programmé pour durer 14 seconde(s). Il a duré 00:00:15.5000000 seconde(s)
A 03:08:02:04, le thread n° 5 se termine. Il était programmé pour durer 15 seconde(s). Il a duré 00:00:25 seconde(s)
A 03:08:04:04, le thread n° 6 se termine. Il était programmé pour durer 16 seconde(s). Il a duré 00:00:27 seconde(s)
  • الأسطر 1-6: يتم تنفيذ المهام الثلاث الأولى بالترتيب. تجد هذه المهام على الفور مؤشر ترابط واحد متاح (MinThreads=3) ثم تدخل في حالة سكون.
  • الأسطر 7-9: بالنسبة للمهمتين 3 و 4، يستغرق الأمر وقتًا أطول قليلاً. لم يكن هناك مؤشر ترابط متاح لكل منهما. كان علينا إنشاء واحد. هذه الآلية ممكنة حتى 5 (MaxThreads=5).
  • السطر 10: لم يعد هناك خيوط متاحة: سيتعين على المهمتين 5 و6 الانتظار.
  • السطور 11-12: تنتهي المهمة 0. تأخذ المهمة 5 خيطها.
  • السطور 13-14: تنتهي المهمة 1. تأخذ المهمة 6 خيطها.
  • الأسطر 17-21: يتم إكمال المهام واحدة تلو الأخرى.

10.8. فئة BackgroundWorker

10.8.1. المثال 1

تنتمي فئة BackgroundWorker إلى مساحة الاسم [System.ComponentModel]. تُستخدم بنفس طريقة استخدام الخيط (thread)، ولكنها تتميز ببعض الميزات الخاصة التي قد تجعلها أكثر إفادة من فئة [Thread] في حالات معينة:

  • تقوم بإصدار الأحداث التالية:
  • DoWork : طلب مؤشر ترابط تنفيذ BackgroundWorker
  • ProgressChanged : قام الكائن BackgroundWorker بتنفيذ ReportProgress. ويُستخدم هذا لإعطاء نسبة مئوية للإنجاز.
  • RunWorkerCompleted: أكمل الكائن BackgroundWorker عمله. وقد يكون أكمله بشكل طبيعي، أو مع إلغاء أو استثناء.

هذه الأحداث تجعل BackgroundWorker مفيدًا في الواجهات الرسومية: سيتم تكليف مهمة تستغرق وقتًا طويلاً إلى BackgroundWorker الذي سيكون قادرًا على الإبلاغ عن تقدمها باستخدام ProgressChanged وعن نهايتها باستخدام الحدث RunWorkerCompleted. سيتم تنفيذ العمل الذي سيقوم به BackgroundWorker بواسطة طريقة مرتبطة بـ DoWork.

  • من الممكن طلب إلغائه. في واجهة رسومية، يمكن للمستخدم إلغاء مهمة طويلة.
  • تنتمي كائنات BackgroundWorker إلى تجمع ويتم إعادة تدويرها حسب الحاجة. سيحصل التطبيق الذي يحتاج إلى BackgroundWorker عليه من التجمع، الذي سيمنحه مؤشر ترابط موجودًا ولكن غير مستخدم. تؤدي إعادة تدوير مؤشرات الترابط بهذه الطريقة، بدلاً من إنشاء مؤشر ترابط جديد في كل مرة، إلى تحسين الأداء.

نستخدم هذه الأداة في التطبيق السابق عندما لا يتم التحكم في الوصول إلى العداد:


using System;
using System.Threading;
using System.ComponentModel;
 
namespace Chap8 {
    class Program2 {
        // use of reader and writer threads
        // illustrates the simultaneous use of shared resources and synchronization
 
         // class variables
        const int nbThreads = 2;                    // total number of threads
        static int nbLecteursTerminés = 0;        // number of terminated threads
        static int[] data = new int[5];            // shared array between reader and writer threads
        static object appli;                            // synchronizes access to number of completed threads
        static Random objRandom = new Random(DateTime.Now.Second);    // a random number generator
        static AutoResetEvent peutLire;        // indicates that the contents of the table can be read
        static AutoResetEvent peutEcrire;        // points out that we can write in the table
        static AutoResetEvent finLecteurs;    // signals the end of readers
 
         //hand
        public static void Main(string[] args) {
 
            // give the thread a name
            Thread.CurrentThread.Name = "Main";
 
             // flag initialization
             peutLire = new AutoResetEvent(fals        e); // cannot be read yet
             peutEcrire = new AutoResetEvent(tru    e); // we can already write
            finLecteurs = new AutoResetEvent(false);    // application not completed
 
             // synchronizes access to terminated thread counter
            appli = new object();                
 
             // creation of reader threads
            MyBackgroundWorker[] lecteurs = new MyBackgroundWorker[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                 // creation
                lecteurs[i] = new MyBackgroundWorker();
                lecteurs[i].Numéro = "L" + i;
                lecteurs[i].DoWork += Lire;
                lecteurs[i].RunWorkerCompleted += EndLecteur;
                 // launch
                lecteurs[i].RunWorkerAsync();
            }
 
            // creating writer threads
            MyBackgroundWorker[] écrivains = new MyBackgroundWorker[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                 // creation
                écrivains[i] = new MyBackgroundWorker();
                écrivains[i].Numéro = "E" + i;
                écrivains[i].DoWork += Ecrire;
                 // launch
                écrivains[i].RunWorkerAsync();
            }
 
            // wait for all threads to finish
            finLecteurs.WaitOne();
             //end of hand
            Console.WriteLine("Fin de Main...");
        }
 
        public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) {
...
        }
 
        // read the contents of the table
        public static void Lire(object sender, DoWorkEventArgs infos) {
...
        }
 
        // write in the table
        public static void Ecrire(object sender, DoWorkEventArgs infos) {
...
        }
    }
 
     // thread
    internal class MyBackgroundWorker : BackgroundWorker {
         // miscellaneous information
        public string Numéro { get; set; }
    }
 
}

نذكر فقط التغييرات بالتفصيل:

  • تم استبدال الفئة Thread بـ MyBackgroundWorker في الأسطر 79-82. تم اشتقاق طريقة الفئة BackgroundWorker لإعطاء الخيط رقمًا. كان بإمكاننا القيام بالأمر بشكل مختلف عن طريق تمرير كائن إلى RunWorkerAsync في الأسطر 43 و 54، كائن يحتوي على رقم الخيط.
  • السطر 58: تنتهي الطريقة Main بعد أن تنتهي جميع خيوط القراءة من عملها. للقيام بذلك، في السطر 12، يقوم العداد nbReadersTerminated بحساب عدد خيوط القراءة التي أكملت عملها. يتم زيادة هذا العداد بواسطة EndLecteur في الأسطر 63-65، والذي يتم تنفيذه في كل مرة ينتهي فيها مؤشر ترابط قارئ. هذا الإجراء هو الذي يتحكم في AutoResetEvent finLecteurs في السطر 18، والذي يتم مزامنته في السطر 59 مع Hand.
  • السطر 16: نظرًا لأن العديد من خيوط القراءة قد ترغب في زيادة العداد nbReadersTerminated في نفس الوقت، يتم توفير وصول حصري إليه بواسطة كائن التزامن app. هذه الحالة غير مرجحة، ولكنها ممكنة نظريًا.
  • الأسطر 35-44: إنشاء خيوط القراءة
  • السطر 38: إنشاء نوع مؤشر الترابط MyBackgroundWorker
  • السطر 39: يتم منحه No
  • السطر 40: تم تكليف دالة Read بالتنفيذ
  • السطر 41: سيتم تنفيذ الطريقة EndLecteur بعد انتهاء الخيط
  • السطر 43: يتم تشغيل الخيط
  • الأسطر 47-55: إنشاء خيوط الكاتب
  • السطر 50: إنشاء مؤشر ترابط من نوع MyBackgroundWorker
  • السطر 51: يُعطى رقمًا
  • السطر 52: يتم تعيين Write له لتنفيذه
  • السطر 54: يتم تشغيل الخيط

تظل الطريقتان Read و Write دون تغيير. يتم تنفيذ الطريقة EndLecteur في نهاية كل مؤشر ترابط قارئ. وفيما يلي كودها:


        public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) {
             // increment no. of completed drives
            lock (appli) {
                nbLecteursTerminés++;
                if (nbLecteursTerminés == nbThreads)
                    finLecteurs.Set();
            }
}

تتمثل وظيفة الطريقة EndLecteur في إخطار Main بأن جميع القراء قد أنجزوا مهمتهم.

  • السطر 4: يتم زيادة العداد nbReadersTerminated.
  • السطران 5-6: إذا أنهى جميع القراء مهمتهم، يتم تعيين الحدث finLecteurs على true لمنع Main من انتظار هذا الحدث.
  • نظرًا لأن EndLecteur يتم تنفيذه بواسطة عدة خيوط، فإن القسم الحرج السابق محمي بواسطة القفل في السطر 3.

يؤدي التنفيذ إلى نتائج مشابهة لتلك الخاصة بالإصدار المتعدد الخيوط.

10.8.2. المثال 2

يوضح الكود التالي نقاطًا أخرى من فئة BackgroundWorker :

  • القدرة على إلغاء المهمة
  • يتم الإبلاغ عن أي استثناء يتم طرحه في المهمة
  • تمرير معلمة I/O إلى المهمة

using System;
using System.Threading;
using System.ComponentModel;
 
namespace Chap8 {
    class Program3 {
 
         // threads
        static BackgroundWorker[] tâches = new BackgroundWorker[5];
 
        public static void Main() {
             // init Current thread
            Thread main = Thread.CurrentThread;
            // name the Thread
            main.Name = "Main";
 
             // thread creation
            for (int i = 0; i < tâches.Length; i++) {
                 // create thread n° i
                tâches[i] = new BackgroundWorker();
                 // initialize it
                tâches[i].DoWork += Sleep;
                tâches[i].RunWorkerCompleted += End;
                tâches[i].WorkerSupportsCancellation = true;
                 // launch it
                tâches[i].RunWorkerAsync(new Data { Numéro = i, Début = DateTime.Now, Durée = i + 1 });
            }
            // cancel the last thread
            tâches[4].CancelAsync();
 
             // end of hand
            Console.WriteLine("Fin du thread {0}, tapez [entrée] pour terminer...", main.Name);
            Console.ReadLine();
            return;
        }
 
        public static void Sleep(object sender, DoWorkEventArgs infos) {
...
        }
 
        public static void End(object sender, RunWorkerCompletedEventArgs infos) {
...
        }
 
        internal class Data {
             // miscellaneous information
            public int Numéro { get; set; }
            public DateTime Début { get; set; }
            public int Durée { get; set; }
            public DateTime Fin { get; set; }
        }
    }
}
  • السطر 9: BackgroundWorker
  • الأسطر 18-27: إنشاء مؤشر الترابط
  • السطر 20: إنشاء مؤشر الترابط
  • السطر 22: سيقوم الخيط بتنفيذ Sleep الأسطر 39-41
  • السطر 23: سيتم تنفيذ الأسلوب End في الأسطر 43-45 في نهاية الخيط
  • السطر 24: يمكن إلغاء الخيط
  • السطر 26: يتم بدء تشغيل الخيط بمعلمة من النوع [Data]، المحددة في الأسطر 49-52. يحتوي هذا الكائن على الحقول التالية:
    • Number (مدخلات): رقم الخيط
    • Start (إدخال): وقت بدء الخيط
    • المدة (مدخلات): مدة تشغيل Sleep
    • النهاية (خروج): نهاية تنفيذ الخيط
  • السطر 29: تم إلغاء الخيط رقم 4

تقوم جميع الخيوط بتنفيذ Sleep التالي:


        public static void Sleep(object sender, DoWorkEventArgs infos) {
             // we use the info parameter
            Data data = (Data)infos.Argument;
             // exception for task no. 3
            if (data.Numéro == 3) {
                throw new Exception("test....");
            }
             // sleep mode for Duration, stopping every second
            for (int i = 1; i <= data.Durée && !tâches[data.Numéro].CancellationPending; i++) {
                 // wait 1 second
                Thread.Sleep(1000);
            }
             // end of execution
            data.Fin = DateTime.Now;
             // initialize the result
            infos.Result = data;
            infos.Cancel = tâches[data.Numéro].CancellationPending;
}
  • السطر 1: تحتوي الطريقة Sleep على توقيع معالج الأحداث القياسي. وهي تستقبل معلمتين:
    • sender : مرسل الحدث، هنا BackgroundWorker الذي ينفذ
    • news : من النوع DoWorkEventArgs الذي يوفر معلومات عن الحدث DoWork. تُستخدم هذه المعلمة لنقل المعلومات إلى الخيط واسترداد نتائجه.
  • السطر 3: المعلمة التي تم تمريرها إلى RunWorkerAsync للمهمة موجودة في infos.Argument.
  • الأسطر 5-7: يتم إلقاء استثناء للمهمة رقم 3
  • الأسطر 9-12: "ينام" الخيط لمدة Duration ثانية بزيادات قدرها ثانية واحدة لتمكين اختبار الإلغاء في السطر 9. هذا يحاكي مهمة طويلة الأمد يقوم خلالها الخيط بفحص طلب الإلغاء بانتظام. للإشارة إلى أنه قد تم إلغاؤها، يجب على الخيط تعيين الخاصية infos.Cancel إلى true (السطر 17).
  • السطر 16: يمكن للخيط إرجاع نتيجة إلى الخيط الذي أطلقه. ويضع هذه النتيجة في infos.Result.

بمجرد الانتهاء، تنفذ الخيوط الأمر End next :


public static void End(object sender, RunWorkerCompletedEventArgs infos) {
            // the infos parameter is used to display the result of execution
             // exception?
            if (infos.Error != null) {
                Console.WriteLine("Le thread {1} a rencontré l'erreur suivante : {0}", infos.Error.Message, sender);
            } else
                if (!infos.Cancelled) {
                    Data data = (Data)infos.Result;
                    Console.WriteLine("Thread {0} terminé : début {1:hh:mm:ss}, durée programmée {2} s, fin {3:hh:mm:ss}, durée effective {4}",
                    data.Numéro, data.Début, data.Durée, data.Fin, (data.Fin - data.Début));
                } else {
                    Console.WriteLine("Thread {0} annulé", sender);
                }
        }
  • السطر 1: تحتوي الطريقة End على توقيع معالج الأحداث القياسي. وهي تستقبل معلمتين:
    • sender : مرسل الحدث، وهو هنا BackgroundWorker الذي ينفذ
    • news : من النوع RunWorkerCompletedEventArgs الذي يوفر معلومات عن الحدث RunWorkerCompleted.
  • السطر 4: يتم ملء الحقل infos.Error من النوع Exception فقط في حالة حدوث استثناء.
  • السطر 7: يتم تعيين الحقل infos.Cancelled من النوع boolean إلى القيمة true إذا تم إلغاء مؤشر الترابط.
  • السطر 8: إذا لم يكن هناك استثناء أو إلغاء، فإن infos.Result هو نتيجة الخيط الذي تم تنفيذه. يؤدي استخدام هذه النتيجة في حالة إلغاء الخيط أو حدوث استثناء إلى حدوث استثناء. وبالتالي، في السطرين 5 و 13، لا يمكننا عرض رقم الخيط الذي تم إلغاؤه أو الذي ألقى استثناءً، لأن هذا الرقم موجود في infos.Result. يمكن التغلب على هذه المشكلة عن طريق اشتقاق فئة BackgroundWorker لتخزين المعلومات المراد تبادلها بين الخيط المستدعي والخيط المستدعى كما في المثال السابق. ثم نستخدم الحجة sender التي تمثل BackgroundWorker بدلاً من news.

والنتائج هي كما يلي:

1
2
3
4
5
6
Fin du thread Main. Laissez les autres threads se terminer puis tapez [entrée] pour terminer...
Thread 0 terminé : début 05:19:46, durée programmée 1 s, fin 05:19:47, durée effective 00:00:01
Le thread System.ComponentModel.BackgroundWorker a rencontré l'erreur suivante : test....
Thread System.ComponentModel.BackgroundWorker annulé
Thread 1 terminé : début 05:19:46, durée programmée 2 s, fin 05:19:49, durée effective 00:00:03
Thread 2 terminé : début 05:19:46, durée programmée 3 s, fin 05:19:50, durée effective 00:00:04

10.9. البيانات المحلية للخيط

10.9.1. المبدأ

لنفترض وجود تطبيق من ثلاث طبقات:

لنفترض أن التطبيق متعدد المستخدمين، مثل تطبيق ويب على سبيل المثال. يتم خدمة كل مستخدم بواسطة مؤشر ترابط مخصص. وتكون دورة حياة مؤشر الترابط كما يلي:

  1. يتم إنشاء الخيط أو طلبه من مجموعة الخيوط لتلبية طلب المستخدم
  2. إذا كان هذا الطلب يتطلب بيانات، فسيقوم الخيط بتنفيذ طريقة من طبقة [ui]، والتي ستستدعي طريقة من طبقة [metier]، والتي بدورها ستستدعي طريقة من طبقة [dao].
  3. يعيد الخيط الاستجابة إلى المستخدم. ثم يختفي أو يتم إعادة تدويره إلى مجموعة الخيوط.

في العملية 2، قد يكون من المثير للاهتمام أن يكون للخيط بياناته الخاصة، أي غير مشتركة مع الخيوط الأخرى. يمكن أن تنتمي هذه البيانات، على سبيل المثال، إلى المستخدم المعين الذي يخدمه الخيط. يمكن بعد ذلك استخدام هذه البيانات في الطبقات المختلفة [ui، metier، dao].

تسمح فئة Thread بهذا السيناريو بفضل نوع من القاموس الخاص الذي تكون مفاتيحه من نوع LocalDataStoreSlot:

ينشئ إدخالاً في القاموس الخاص للخيط باسم المفتاح.
يربط بيانات القيمة باسم المفتاح من القاموس الخاص للخيط
يسترد القيمة المرتبطة بالاسم من القاموس الخاص للخيط

قد يكون نموذج الاستخدام كما يلي:

  • لإنشاء زوج (مفتاح، قيمة) مرتبط بالخيط الحالي:
Thread.SetData(Thread.GetNamedDataSlot("clé"),valeur);
  • لاسترداد القيمة المرتبطة بالمفتاح:
Thread.GetData(Thread.GetNamedDataSlot("clé"));

10.9.2. تطبيق المبدأ

لنأخذ التطبيق ذي الطبقات الثلاث التالي:

لنفترض أن طبقة [dao] تدير قاعدة بيانات للمقالات وأن واجهتها في البداية كما يلي:


using System.Collections.Generic;
 
namespace Chap8 {
    public interface IDao {
        int InsertArticle(Article article);
        List<Article> GetAllArticles();
        void DeleteAllArticles();
    }
}
  • السطر 5: لإدراج عنصر في قاعدة البيانات
  • السطر 6: لاسترداد جميع المقالات الموجودة في قاعدة البيانات
  • السطر 7: لحذف جميع المقالات من قاعدة البيانات

لاحقًا، سنحتاج إلى طريقة لإدراج مصفوفة من المقالات باستخدام معاملة، لأننا نريد العمل في وضع "كل شيء أو لا شيء": إما إدراج جميع المقالات، أو عدم إدراج أي منها على الإطلاق. يمكننا بعد ذلك تعديل الواجهة لدمج هذا المطلب الجديد:


using System.Collections.Generic;
 
namespace Chap8 {
    public interface IDao {
        int InsertArticle(Article article);
        void insertArticles(Article[] articles);
        List<Article> GetAllArticles();
        void DeleteAllArticles();
    }
}
  • السطر 6: لإضافة مصفوفة المقالات إلى قاعدة البيانات

لاحقًا، في تطبيق آخر، تنشأ الحاجة إلى حذف قائمة من المقالات المحفوظة في قائمة، وذلك في إطار معاملة واحدة. وكما نرى، ستتوسع طبقة [dao] لتلبية احتياجات العمل المختلفة. ويمكننا اتباع مسار آخر:

  • وضع العمليات الأساسية فقط في طبقة [dao] InsertArticle، DeleteArticle، UpdateArticle، SelectArticle، SelectArticles
  • نقل التحديث المتزامن لعدة مقالات إلى طبقة [business]. ستستخدم هذه العمليات العمليات الأساسية لطبقة [dao].

ميزة هذا الحل هي أنه يمكن استخدام نفس طبقة [dao] دون تغيير مع طبقات [metier] مختلفة. ومع ذلك، فإنه يسبب صعوبة في إدارة المعاملة، التي تجمع التحديثات التي سيتم إجراؤها بشكل متكامل على:

  • يجب أن تبدأ المعاملة من طبقة [metier] قبل أن تستدعي أساليب طبقة [dao]
  • يجب أن تكون الطرق في طبقة [dao] على علم بوجود المعاملة من أجل المشاركة فيها إذا كانت موجودة
  • يجب إنهاء المعاملة بواسطة طبقة [business].

لضمان أن تكون أساليب طبقة [dao] على علم بوجود أي معاملة حالية، يمكننا إضافة المعاملة كمعلمة لكل أسلوب في طبقة [dao]. ستظهر هذه المعلمة بعد ذلك في توقيع أساليب الواجهة، لربطها بمصدر بيانات محدد: قاعدة البيانات. توفر لنا البيانات المحلية للخيط حلاً أكثر أناقة: ستضع طبقة [business] المعاملة في البيانات المحلية للخيط، وستقوم طبقة [dao] بجلبها من هناك. لا يلزم تغيير توقيع طريقة طبقة [dao].

نقوم بتنفيذ هذا الحل باستخدام مشروع Visual Studio التالي:

  • في [1]: الحل ككل
  • في [2]: المراجع المستخدمة. وبما أن [4] عبارة عن قاعدة بيانات SQL Server Compact، فإن مرجع [System.Data.SqlServerCe] مطلوب.
  • في [3]: الطبقات المختلفة للتطبيق.

الأساس [4] هو قاعدة بيانات SQL Server Compact التي تم استخدامها بالفعل في الفصل السابق، وبالتحديد في الفقرة 9.3.1.

 

فئة Article

يتم تغليف صف من الجدول السابق [articles] في كائن من نوع Article :


namespace Chap8 {
    public class Article {
         // properties
        public int Id { get; set; }
        public string Nom { get; set; }
        public decimal Prix { get; set; }
        public int StockActuel { get; set; }
        public int StockMinimum { get; set; }
 
         // manufacturers
        public Article() { 
        }
 
        public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) {
            Id = id;
            Nom = nom;
            Prix = prix;
            StockActuel = stockActuel;
            StockMinimum = stockMinimum;
        }
 
         // identity
        public override string ToString() {
            return string.Format("[{0},{1},{2},{3},{4}]", Id, Nom, Prix, StockActuel, StockMinimum);
        }
    }
}

واجهة الطبقة [dao]

ستكون واجهة IDao لطبقة [dao] كما يلي:


using System.Collections.Generic;
 
namespace Chap8 {
    public interface IDao {
        int InsertArticle(Article article);
        List<Article> GetAllArticles();
        void DeleteAllArticles();
    }
}
  • السطر 5: لإدراج عنصر في الجدول [articles]
  • السطر 6: لوضع جميع صفوف الجدول [articles] في قائمة كائنات Article
  • السطر 7: لحذف جميع الأسطر في الجدول [articles]

واجهة الطبقة [metier]

ستكون واجهة IMetier لطبقة [metier] كما يلي:


using System.Collections.Generic;
 
namespace Chap8 {
    interface IMetier {
        void InsertArticlesInTransaction(Article[] articles);
        void InsertArticlesOutOfTransaction(Article[] articles);
        List<Article> GetAllArticles();
        void DeleteAllArticles();
    }
}
  • السطر 5: لإدراج مجموعة من المقالات ضمن معاملة
  • السطر 6: نفس الشيء ولكن بدون معاملة
  • السطر 7: للحصول على قائمة بجميع المقالات
  • السطر 8: لحذف جميع المقالات

تنفيذ طبقة [metier]

سيكون تنفيذ واجهة التجارة IMetier على النحو التالي:


using System.Collections.Generic;
using System.Data;
using System.Data.SqlServerCe;
using System.Threading;
 
namespace Chap8 {
    public class Metier : IMetier {
         // layer [dao]
        public IDao Dao { get; set; }
         // connecting chain
        public string ConnectionString { get; set; }
 
        // insert an array of articles inside a transaction
        public void InsertArticlesInTransaction(Article[] articles) {
            // create the connection to the
            using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
                 // opening connection
                connexion.Open();
                 // transaction
                SqlCeTransaction transaction = null;
                try {
                     // start of transaction
                    transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);
                    // register the transaction in the thread
                    Thread.SetData(Thread.GetNamedDataSlot("transaction"), transaction);
                     // articles insertion
                    foreach (Article article in articles) {
                        Dao.InsertArticle(article);
                    }
                    // validate the transaction
                    transaction.Commit();
                } catch {
                    // we undo the transaction
                    if (transaction != null)
                        transaction.Rollback();
                }
            }
        }
 
        // insertion of an array of articles without transaction
        public void InsertArticlesOutOfTransaction(Article[] articles) {
             // articles insertion
            foreach (Article article in articles) {
                Dao.InsertArticle(article);
            }
        }
 
         // articles list
        public List<Article> GetAllArticles() {
            return Dao.GetAllArticles();
        }
         // delete all articles
        public void DeleteAllArticles() {
            Dao.DeleteAllArticles();
        }
    }
}

تحتوي الفئة على الخصائص التالية:

  • السطر 9: مرجع إلى طبقة [dao]
  • السطر 11: سلسلة الاتصال المستخدمة للاتصال بقاعدة بيانات المقالات

نكتفي بالتعليق على الطريقة InsertArticlesInTransaction التي تنطوي وحدها على صعوبات:

  • السطر 16: يتم إنشاء اتصال بقاعدة البيانات
  • السطر 18: الآن افتح
  • السطر 23: يتم إنشاء معاملة
  • السطر 25: يتم الحفظ في البيانات المحلية للخيط، المرتبطة بمفتاح "transaction"
  • الأسطر 27-29: يتم استدعاء طريقة إدراج وحدة طبقة [dao] لكل عنصر سيتم إدراجه
  • السطران 21 و 32: يتم التحكم في عملية إدراج المصفوفة بالكامل بواسطة try / catch
  • السطر 31: إذا وصلت إلى هذه النقطة، فهذا يعني أنه لم تحدث أي استثناءات. ثم يتم التحقق من صحة المعاملة.
  • السطران 34-35: حدث استثناء، يتم التراجع عن المعاملة
  • السطر 37: الخروج من الجملة باستخدام. يتم إغلاق الاتصال الذي تم فتحه في السطر 18 تلقائيًا.

تنفيذ طبقة [dao]

سيكون تنفيذ واجهة Dao IDao على النحو التالي:


using System.Collections.Generic;
using System.Data;
using System.Data.SqlServerCe;
using System.Threading;
 
namespace Chap8 {
    public class Dao : IDao {
         // connecting chain
        public string ConnectionString { get; set; }
         // requests
        public string InsertText { get; set; }
        public string DeleteAllText { get; set; }
        public string GetAllText { get; set; }
 
         // interface implementation

         // article insertion
        public int InsertArticle(Article article) {
            // is there a transaction in progress?
            SqlCeTransaction transaction = Thread.GetData(Thread.GetNamedDataSlot("transaction")) as SqlCeTransaction;
            // retrieve or create connection
            SqlCeConnection connexion = null;
            if (transaction != null) {
                 // recover connection
                connexion = transaction.Connection as SqlCeConnection;
            } else {
                 // create it
                connexion = new SqlCeConnection(ConnectionString);
                connexion.Open();
            }
            try {
                 // preparation of insertion order
                SqlCeCommand sqlCommand = new SqlCeCommand();
                sqlCommand.Transaction = transaction;
                sqlCommand.Connection = connexion;
                sqlCommand.CommandText = InsertText;
                sqlCommand.Parameters.Add("@nom", SqlDbType.NVarChar, 30);
                sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
                sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
                sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
                sqlCommand.Parameters["@nom"].Value = article.Nom;
                sqlCommand.Parameters["@prix"].Value = article.Prix;
                sqlCommand.Parameters["@sa"].Value = article.StockActuel;
                sqlCommand.Parameters["@sm"].Value = article.StockMinimum;
                 // execution
                return sqlCommand.ExecuteNonQuery();
            } finally {
                // if you were not in a transaction, you close the connection
                if (transaction == null) {
                    connexion.Close();
                }
            }
        }
 
         // articles list
        public List<Article> GetAllArticles() {
...
        }
 
         // deletion of articles
        public void DeleteAllArticles() {
...
        }
    }
}

تحتوي الفئة على الخصائص التالية:

  • السطر 9: سلسلة الاتصال المستخدمة للاتصال بقاعدة بيانات المقالات
  • السطر 11: أمر SQL لإدراج عنصر
  • السطر 12: أمر SQL لحذف جميع المقالات
  • السطر 13: أمر SQL للحصول على جميع المقالات

سيتم تهيئة هذه الخصائص من ملف التكوين التالي [App.config]:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
    </connectionStrings>
    <appSettings>
        <add key="insertText" value="insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)"/>
        <add key="getAllText" value="select id,nom,prix,stockactuel,stockminimum from articles"/>
        <add key="deleteAllText" value="delete from articles"/>
    </appSettings>
</configuration>

نعلق على الطريقة InsertArticle :

  • السطر 20: يستعيد أي معاملات تم وضعها بواسطة طبقة [metier] في الخيط
  • الأسطر 23-25: إذا كانت المعاملة موجودة، يتم استرداد الاتصال الذي كانت مرتبطة به.
  • الأسطر 26-30: خلاف ذلك، يتم إنشاء اتصال جديد وفتحه.
  • الأسطر 33-44: إعداد أمر الإدراج. يتم تحديد معالم هذا الأمر (انظر السطر g في App.config).
  • السطر 33: يتم إنشاء الكائن Command.
  • السطر 34: يتم ربطه بالمعاملة الحالية. إذا لم تكن المعاملة الحالية موجودة (transaction=null)، فإن هذا يعادل تنفيذ أمر SQL بدون معاملة صريحة. في هذه الحالة، لا تزال هناك معاملة ضمنية. مع SQL Server CE، يتم تعيين هذه المعاملة الضمنية افتراضيًا على الوضع autocommit : يتم تنفيذ أمر SQL بعد التنفيذ.
  • السطر 35: يتم ربط الكائن Command بالاتصال الحالي
  • السطر 36: يتم تعيين نص SQL المراد تنفيذه. هذا هو الاستعلام المعلم في السطر g من App.config.
  • الأسطر 37-44: يتم تهيئة معلمات الاستعلام الأربعة
  • السطر 46: يتم تنفيذ الطلب.
  • الأسطر 49-51: تذكر أنه في حالة عدم وجود معاملة، تم فتح اتصال جديد مع القاعدة، الأسطر 26-30. في هذه الحالة، يجب إغلاقه. إذا كانت هناك معاملة، فلا يجب إغلاق الاتصال، حيث إن طبقة [metier] هي التي تديره.

تعتمد الطريقتان الأخريان على ما رأيناه في فصل "قواعد البيانات":


         // list of items
        public List<Article> GetAllArticles() {
             // item list - empty at start
            List<Article> articles = new List<Article>();
             // operation connection
            using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
                 // opening connection
                connexion.Open();
                 // executes sqlCommand with select query
                SqlCeCommand sqlCommand = new SqlCeCommand(GetAllText, connexion);
                using (SqlCeDataReader reader = sqlCommand.ExecuteReader()) {
                     // operating income
                    while (reader.Read()) {
                         // current line operation
                        articles.Add(new Article(reader.GetInt32(0), reader.GetString(1), reader.GetDecimal(2), reader.GetInt32(3), reader.GetInt32(4)));
                    }
                }
            }
             // we return the result
            return articles;
        }
 
         // article deletion
        public void DeleteAllArticles() {
            using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
                 // opening connection
                connexion.Open();
                 // executes sqlCommand with update request
                new SqlCeCommand(DeleteAllText, connexion).ExecuteNonQuery();
            }
}

تطبيق الاختبار [console]

تطبيق [وحدة التحكم] الاختباري هو كما يلي:


using System;
using System.Configuration;
 
namespace Chap8 {
    class Program {
        static void Main(string[] args) {
            // using the configuration file
            string connectionString = null;
            string insertText;
            string getAllText;
            string deleteAllText;
            try {
                 // connecting chain
                connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
                 // other parameters
                insertText = ConfigurationManager.AppSettings["insertText"];
                getAllText = ConfigurationManager.AppSettings["getAllText"];
                deleteAllText = ConfigurationManager.AppSettings["deleteAllText"];
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }
             // layer creation [dao]
            Dao dao = new Dao();
            dao.ConnectionString = connectionString;
            dao.DeleteAllText = deleteAllText;
            dao.GetAllText = getAllText;
            dao.InsertText = insertText;
             // layer creation [job]
            Metier metier = new Metier();
            metier.Dao = dao;
            metier.ConnectionString = connectionString;
            // we create an array of articles
            Article[] articles = new Article[2];
            for (int i = 0; i < articles.Length; i++) {
                articles[i] = new Article(0, "article", 100, 10, 1);
            }
             // we delete all articles
            Console.WriteLine("Suppression de tous les articles...");
            metier.DeleteAllArticles();
            // insert the table outside the transaction
            Console.WriteLine("Insertion des articles hors transaction...");
            try {
                metier.InsertArticlesOutOfTransaction(articles);
            } catch (Exception e){
                Console.WriteLine("Exception : {0}", e.Message);
            }
            // we display the articles
            Console.WriteLine("Liste des articles");
            AfficheArticles(metier);
             // we delete all articles
            Console.WriteLine("Suppression de tous les articles...");
            metier.DeleteAllArticles();
            // insert the array in a transaction
            Console.WriteLine("Insertion des articles dans une transaction...");
            metier.InsertArticlesInTransaction(articles);
            // we display the articles
            Console.WriteLine("Liste des articles");
            AfficheArticles(metier);
        }
 
        private static void AfficheArticles(IMetier metier) {
            // we display the articles
            foreach(Article article in metier.GetAllArticles()){
                Console.WriteLine(article);
            }
        }
 
    }
}
  • الأسطر 12-22: يتم استخدام الملف [App.config].
  • الأسطر 24-28: يتم إنشاء مثيل للطبقة [dao] وتهيئتها
  • الأسطر 30-32: ينطبق الأمر نفسه على طبقة [metier]
  • الأسطر 34-37: إنشاء جدول من مقالتين يحملان نفس الاسم. الجدول [articles] في قاعدة بيانات خادم SQL هذا [dbarticles.sdf] له قيد تفرد على الاسم. وبالتالي، سيتم رفض إدراج العنصر الثاني. إذا تم إدراج المصفوفة خارج المعاملة، فسيتم إدراج العنصر الأول أولاً، ثم يبقى مدرجًا. إذا تم إدراج المصفوفة في معاملة، فسيتم إدراج العنصر الأول أولاً، ثم إزالته عند تنفيذ المعاملة. التراجع عن المعاملة.
  • الأسطر 39-50: إدراج مصفوفتي مقالات خارج المعاملة والتحقق.
  • الأسطر 52-59: كما هو مذكور أعلاه، ولكن ضمن معاملة

النتائج هي كما يلي:

1
2
3
4
5
6
7
8
9
Suppression de tous les articles...
Insertion des articles hors transaction...
Exception : A duplicate value cannot be inserted into a unique index. [ Table na
me = ARTICLES,Constraint name = UQ__ARTICLES__0000000000000010 ]
Liste des articles
[7,article,100,10,1]
Suppression de tous les articles...
Insertion des articles dans une transaction...
Liste des articles
  • السطران 5-6: أدى الإدراج خارج المعاملة إلى ترك العنصر الأول في قاعدة البيانات
  • السطر 9: الإدراج في معاملة لم يترك أي عناصر في قاعدة البيانات

10.9.3. الخلاصة

أظهر المثال السابق مزايا البيانات المحلية للخيط (thread-local) في إدارة المعاملات. ولا ينبغي تكراره كما هو. تستخدم أطر العمل مثل Spring وNhibernate وغيرها هذه التقنية، لكنها تجعلها أكثر شفافية: فمن الممكن لطبقة [metier] استخدام المعاملات دون الحاجة إلى علم طبقة [dao]. لا توجد معاملات في كود طبقة [dao]. يتم تحقيق ذلك باستخدام تقنية وكيل تسمى AOP (البرمجة الموجهة للجوانب). مرة أخرى، نحثك على استخدام هذه الأطر.

10.10. لمعرفة المزيد...

للحصول على نظرة أكثر تعمقًا في مجال تزامن الخيوط الصعب، اقرأ الفصل "Threading" من كتاب C# 3.0 المشار إليه في مقدمة هذا المستند. فهو يعرض العديد من تقنيات التزامن لأنواع مختلفة من المواقف.