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-7: العرض كل ثانية
- السطر 8: تم إيقاف البرنامج بواسطة Ctrl-C.
10.2. إنشاء خيوط التنفيذ
من الممكن أن تكون هناك تطبيقات تعمل فيها أجزاء من الكود "بشكل متزامن" في خيوط تنفيذ مختلفة. عندما نقول أن الخيوط تعمل بشكل متزامن، فغالبًا ما يكون هذا مصطلحًا خاطئًا. إذا كان الجهاز يحتوي على معالج واحد فقط، كما هو الحال في كثير من الأحيان، فإن الخيوط تتشارك هذا المعالج: حيث يمكن لكل منها الوصول إليه، بالتناوب، لفترة قصيرة (بضع ميلي ثوانٍ). وهذا يعطي انطباعًا زائفًا بالتنفيذ المتوازي. يعتمد الجزء من الوقت المخصص لخيط على عوامل مختلفة، بما في ذلك أولويته، التي لها قيمة افتراضية ولكن يمكن أيضًا تعيينها برمجيًا. عندما يكون الخيط هو صاحب المعالج، فإنه يستخدمه بشكل طبيعي طوال الوقت المخصص له. ومع ذلك، يمكنه تحريره مبكرًا:
- عن طريق انتظار حدث (Wait، Join)
- عن طريق وضع نفسه في حالة سكون لفترة زمنية محددة (Sleep)
- يتم إنشاء مؤشر الترابط T أولاً بواسطة أحد المُصنّعين المذكورين أعلاه، على سبيل المثال:
حيث Start هي طريقة ذات أحد التوقيعين التاليين:
إنشاء مؤشر ترابط لا يعني تشغيله.
- يتم تشغيل الخيط T بواسطة T.Start() : وعندئذٍ سيقوم الخيط T بتنفيذ الطريقة Start التي تم تمريرها إلى مُنشئ T. ولا ينتظر البرنامج الذي ينفذ T.Start() انتهاء المهمة T: بل ينتقل فورًا إلى التعليمات التالية. وهذا يعني أن مهمتين تعملان بالتوازي. وفي كثير من الحالات، يتعين أن تكونا قادرتين على التواصل مع بعضهما البعض لمتابعة تقدم عملهما المشترك. وهذه هي مشكلة تزامن الخيوط.
- بمجرد إطلاقه، يعمل الخيط T بشكل مستقل. سيتوقف عندما تنتهي المهمة التي ينفذها Start من عملها.
- يمكن إجبار مؤشر الترابط T على الإنهاء:
- يطلب T.Abort() من الخيط T الإنهاء.
- يمكنك أيضًا انتظار انتهاء تنفيذه بواسطة 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] لمدة ثانية واحدة. ثم سيتم منح المعالج لمؤشر ترابط آخر ينتظر المعالج. في نهاية ثانية التوقف، سيكون مؤشر الترابط المتوقف مرشحًا للمعالج. وسيحصل عليه عندما يحين دوره. يعتمد هذا على عوامل مختلفة، بما في ذلك أولوية مؤشرات الترابط الأخرى التي تنتظر المعالج.
والنتائج هي كما يلي:
هذه النتائج مفيدة للغاية:
- أولاً، يمكننا أن نرى أن بدء تنفيذ مؤشر ترابط لا يؤدي إلى حجب. فقد بدأ 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-5: تبدأ الخيوط التي أنشأها Main التنفيذ ويتم مقاطعتها لمدة ثانية واحدة
- السطر 6: يستعيد مؤشر الترابط [Main] المعالج وينفذ التعليمات:
توقف هذه التعليمات جميع الخيوط وليس 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، تكون جميع مؤشرات الترابط الخمسة التي بدأها قد انتهت.
النتائج هي كما يلي:
- السطر 11: انتهى مؤشر الترابط [Main] بعد انتهاء مؤشرات الترابط التي بدأها.
10.3. فوائد الخيوط
الآن بعد أن أبرزنا وجود مؤشر ترابط افتراضي، وهو الذي ينفذ Main، ونعرف كيفية إنشاء مؤشرات ترابط جديدة، دعونا نلقي نظرة على ما تعنيه مؤشرات الترابط بالنسبة لنا ولماذا نقدمها هنا. هناك نوع واحد من التطبيقات يناسب استخدام مؤشرات الترابط بشكل جيد، وهو تطبيقات العميل-الخادم على الإنترنت. سنقدمها في الفصل التالي. في تطبيق العميل-الخادم على الإنترنت، يستجيب خادم على الجهاز S1 لطلبات العملاء على الأجهزة البعيدة C1، C2، ...، Cn.
![]() |
نستخدم يوميًا تطبيقات الإنترنت التي تتوافق مع هذا الرسم التخطيطي: خدمات الويب، والبريد الإلكتروني، وتصفح المنتديات، ونقل الملفات... في الرسم التخطيطي أعلاه، يجب أن يخدم الخادم S1 عملاء Ci في وقت واحد. إذا أخذنا مثال خادم FTP (بروتوكول نقل الملفات) الذي يوصل الملفات إلى عملائه، فإننا نعلم أن نقل الملفات قد يستغرق أحيانًا عدة دقائق. وبالطبع، من المستحيل أن يحتكر عميل ما الخادم طوال هذه المدة. ما يحدث عادةً هو أن يقوم الخادم بإنشاء عدد من سلاسل التنفيذ يساوي عدد العملاء. ثم يتولى كل سلسلة التعامل مع عميل معين. ونظرًا لأن المعالج يتم تقاسمه بشكل دوري بين جميع سلاسل الجهاز النشطة، فإن الخادم يقضي وقتًا قصيرًا مع كل عميل، مما يضمن الخدمة المتزامنة.
![]() |
في الممارسة العملية، يستخدم الخادم مجموعة مؤشرات ترابط ذات عدد محدود من مؤشرات الترابط، 50 على سبيل المثال. ثم يُطلب من العميل رقم 51 الانتظار.
10.4. تبادل المعلومات بين الخيوط
في الأمثلة السابقة، تم تهيئة مؤشر الترابط على النحو التالي:
حيث كانت Run طريقة ذات التوقيع التالي:
من الممكن أيضًا استخدام التوقيع التالي:
وهذا يسمح بنقل المعلومات إلى الخيط الذي تم تشغيله. على سبيل المثال،
سيطلق 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 الخاص بالمعلمة
والنتائج هي كما يلي:
يوضح هذا المثال أن خيطين يمكنهما تبادل المعلومات:
- يمكن لسلسلة التشغيل المشغلة التحكم في تنفيذ سلسلة التشغيل التي تم تشغيلها من خلال تزويدها بالمعلومات
- يمكن للخيط الذي تم تشغيله إرجاع النتائج إلى الخيط المشغل.
لكي يعرف الخيط الذي تم تشغيله متى تتوفر النتائج التي ينتظرها، يجب تنبيهه عند انتهاء الخيط الذي تم تشغيله. هنا، انتظر انتهاءه باستخدام 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.
- السطر 62: يتم قراءة العداد
- السطر 66: يتوقف الخيط لمدة ثانية واحدة. وبالتالي يفقد المعالج
- السطر 68: يتم زيادة العداد
الخطوة 2 موجودة فقط لإجبار الخيط على فقدان المعالج. سيتم منح المعالج لخيط آخر. في الممارسة العملية، ليس هناك ما يضمن أن الخيط لن يتم مقاطعته بين لحظة قراءة العداد ولحظة زيادته. حتى إذا كتبت cptrThreads++، مما يعطي انطباعًا بوجود تعليمة واحدة، فهناك خطر فقدان المعالج بين قراءة قيمة العداد وكتابة قيمته المضافة بـ 1. في الواقع، ستكون العملية عالية المستوى cptrThreads++ موضوعًا لعدة تعليمات أولية على مستوى المعالج. وبالتالي، فإن مرحلة السكون لمدة ثانية واحدة في الخطوة 2 موجودة فقط لتنظيم هذا الخطر.
النتائج التي تم الحصول عليها باستخدام 5 خيوط هي كما يلي:
بقراءة هذه النتائج، من السهل معرفة ما يحدث:
- السطر 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 لتعريف قسم حرج على النحو التالي:
يجب أن يكون 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 خيوط هي كما يلي:
- الخيط 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 بمزامنة الخيوط من تطبيقات مختلفة.
سنستخدم المنشئ والطرق التالية:
ينشئ Mutex M | |
يطلب مؤشر الترابط T1 الذي ينفذ M.WaitOne() خاصية كائن التزامن M. إذا لم يكن مؤشر الترابط M محتفظًا به أي مؤشر ترابط (كما كان في البداية)، يتم "منحه" لمؤشر الترابط T1 الذي طلبه. إذا قام مؤخراً مؤشر ترابط T2 بتنفيذ نفس العملية، فسيتم حظره. وذلك لأن Mutex لا يمكن أن ينتمي إلا إلى مؤشر ترابط واحد. وسيتم إلغاء قفله عندما يقوم مؤشر الترابط T1 بتحرير Mutex M الذي يحتفظ به. وبالتالي، يمكن حظر عدة مؤشرات ترابط أثناء انتظار Mutex M. | |
يتخلى الخيط T1 الذي يقوم بتنفيذ M.ReleaseMutex() عن ملكية Mutex M. عندما يفقد الخيط T1 المعالج، يمكن للنظام منحه لأحد الخيوط التي تنتظر Mutex M. سيحصل عليه خيط واحد فقط بالترتيب، بينما ستظل الخيوط الأخرى التي تنتظر M محجوبة |
يدير Mutex M الوصول إلى المورد المشترك R. يطلب الخيط المورد R بواسطة M.WaitOne() ويقوم بإطلاقه بواسطة 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 على النحو التالي:
تشير الحالة المنطقية إلى ما إذا كان الحاجز مغلقًا (false) أم مفتوحًا (true). سيشير الخيط الذي يرغب في عبور الحاجز إلى ذلك على النحو التالي:
- إذا كان الحاجز مفتوحًا، يمر الخيط ويُغلق الحاجز من خلفه. إذا كان هناك عدة خيوط في انتظار، يمكننا التأكد من أن خيطًا واحدًا فقط سيمر.
- إذا كان الحاجز مغلقًا، يتم حظر الخيط. وسيقوم خيط آخر بفتحه عندما يحين الوقت المناسب. ويعتمد هذا الوقت كليًا على المشكلة التي يتم معالجتها. وسيتم فتح الحاجز بواسطة العملية:
قد يحدث أن يرغب مؤشر ترابط في إغلاق حاجز. ويمكنه القيام بذلك عن طريق:
إذا استبدلنا، في المثال السابق، الكائن 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 الطرق الثابتة التالية:

تحتوي الطريقة Incrementally على التوقيع التالي:
تقوم هذه الطريقة بزيادة قيمة الإيجار. ويضمن أن تكون هذه العملية متجانسة.
يمكن أن يكون برنامج عد الخيوط لدينا كما يلي:
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 لتجنب التداخل.
- لدينا مصفوفة تقوم فيها بعض الخيوط بإيداع البيانات (الكتاب) وتقوم أخرى بقراءتها (القراء).
- الكتاب متساوون ولكنهم حصريون: يمكن لكتاب واحد فقط في كل مرة إدخال البيانات في الجدول.
- القراء متساوون ولكنهم حصريون: يمكن لقارئ واحد فقط في كل مرة قراءة البيانات المخزنة في الجدول.
- لا يمكن للقارئ قراءة البيانات الموجودة في الجدول إلا بعد أن يقوم كاتب بإيداع البيانات فيه، ولا يمكن للكاتب إيداع بيانات جديدة في الجدول إلا بعد أن يقرأ القارئ البيانات الموجودة فيه.
يمكن التمييز بين موردين مشتركين:
- لوحة الكتابة: لا يمكن الوصول إليها إلا لكاتب واحد في كل مرة.
- لوحة العرض للقراءة فقط: لا يمكن الوصول إليها إلا لقارئ واحد في كل مرة.
وترتيب استخدام هذه الموارد:
- يجب أن يأتي القارئ دائمًا بعد الكاتب.
- يجب أن يأتي الكاتب دائمًا بعد القارئ، باستثناء المرة الأولى.
يمكن التحكم في الوصول إلى هذين الموردين باستخدام حاجزين من نوع 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: يُعلم خيوط القراءة بأن المصفوفة قد تم ملؤها ويمكن قراءتها مرة أخرى.
يؤدي التنفيذ إلى النتائج التالية:
تجدر الإشارة إلى النقاط التالية:
- لا يوجد سوى محرك أقراص واحد في كل مرة، على الرغم من أنه يفقد المعالج في القسم الحرج "قراءة"
- لا يوجد سوى كاتب واحد في كل مرة، على الرغم من أنه يفقد المعالج في القسم المراجعة Write
- لا يقرأ القارئ إلا عندما يكون هناك شيء لقراءته في الجدول
- لا يكتب الكاتب حتى يتم قراءة الصورة بالكامل
10.6.2. فئة Monitor
في المثال السابق:
- هناك موردان مشتركان يجب إدارتهما
- بالنسبة لمورد معين، تكون الخيوط متساوية.
عندما يتم حظر خيوط الكاتب في peutEcrire.WaitOne، يتم إلغاء قفل إحداها، أي واحدة منها، بواسطة العملية peutEcrire.Set. إذا كانت العملية السابقة تنطوي على فتح البوابة لكاتب معين، فإن الأمور تصبح أكثر تعقيدًا.
التشبيه هو بمؤسسة تخدم الجمهور عند الكاونترات، حيث كل كاونتر متخصص. عندما يصل العملاء، يأخذون تذكرة من آلة إصدار التذاكر للكاونتر X ثم يجلسون. كل تذكرة مرقمة، ويتم استدعاء العملاء برقمهم عبر مكبر الصوت. أثناء الانتظار، يمكن للعملاء أن يفعلوا ما يشاؤون. يمكنهم القراءة أو الغفوة. في كل مرة، يستيقظ العميل على صوت مكبر الصوت الذي يعلن أن الرقم Y قد تم استدعاؤه إلى الكاونتر X. إذا كان هو المقصود، ينهض العميل ويتوجه إلى الكاونتر X، وإلا فإنه يواصل ما كان يفعله.
يمكننا أن نفعل ذلك هنا بطريقة مماثلة. خذ الكتّاب على سبيل المثال:
يتم حظر سلاسلهم | |
يخبر الخيط الذي كان يقرأ المصفوفة الكتّاب بأن المصفوفة متاحة. يقوم هو أو خيط آخر بتعيين خيط الكاتب لعبور الحاجز. | |
يتحقق كل مؤشر ترابط لمعرفة ما إذا كان هو المختار. إذا كان كذلك، فإنه يتجاوز الحاجز. إذا لم يكن كذلك، فإنه يعود إلى وضع الانتظار. |
تُستخدم فئة Monitor لتنفيذ هذا السيناريو.

نصف الآن بنية قياسية (نمط)، مقترحة في الفصل "التعدد الخيطي" من كتاب C# 3.0 المشار إليه في مقدمة هذا المستند، قادرة على حل مشاكل الحواجز ذات شروط الدخول.
- أولاً، تصل الخيوط التي تتشارك مورداً (العداد، إلخ) إليه عبر كائن سنسميه رمزاً. لفتح البوابة المؤدية إلى العداد، يجب أن يكون لديك الرمز لفتحه، ولا يوجد سوى رمز واحد. لذلك، يجب أن تتبادل الخيوط الرمز فيما بينها.
- للوصول إلى العداد، تطلب الخيوط أولاً:
إذا كان الرمز متاحًا، يتم منحه للخيط الذي نفذ العملية السابقة، وإلا يتم تعليق الخيط في انتظار الرمز.
- إذا كان الوصول إلى العداد غير مرتب، أي إذا كان الشخص الذي يدخل لا يهم، فإن العملية السابقة كافية. يذهب الخيط الذي يحمل الرمز إلى العداد. إذا كان الوصول مرتبًا، يتحقق الخيط الذي يحمل الرمز من أنه يستوفي شرط الذهاب إلى العداد:
إذا لم يكن الخيط هو الخيط المتوقع عند العداد، فإنه يتنازل عن دوره عن طريق إعادة الرمز المميز. ويدخل في حالة حظر. وسيتم إيقاظه بمجرد أن يصبح الرمز المميز متاحًا مرة أخرى. ثم سيتحقق مرة أخرى مما إذا كان يستوفي شرط الذهاب إلى العداد. لا يمكن إجراء العملية Monitor.Wait(token) التي تطلق الرمز المميز إلا إذا كان الخيط يمتلك الرمز المميز. إذا لم يكن الأمر كذلك، يتم إصدار استثناء.
- يذهب الخيط الذي يتحقق من الشرط للذهاب إلى العداد إلى هناك:
- // عمل العداد
- ....
قبل مغادرة العداد، يجب أن يعيد الخيط رمزه، وإلا فإن الخيوط المحجوبة في انتظار ذلك ستظل محجوبة إلى أجل غير مسمى. هناك حالتان مختلفتان:
- الحالة الأولى هي عندما يكون الخيط الذي يحمل الرمز هو نفسه الذي يُعلم الخيوط التي تنتظر الرمز بأنه أصبح متاحًا. وسيقوم بذلك على النحو التالي:
في السطر 6، يتم إيقاظ الخيوط التي تنتظر الرمز. وهذا يعني أنها تصبح مؤهلة لتلقي الرمز. ولا يعني ذلك أنها تتلقاه على الفور. في السطر 8، يتم تحرير الرمز. ستتلقى جميع الخيوط المؤهلة الرمز بالترتيب، بشكل غير محدد. سيعطيهم هذا الفرصة للتحقق مرة أخرى مما إذا كانوا يستوفون شرط الوصول. قامت الخيط التي حررت الرمز بتعديل هذا الشرط في السطر 4 للسماح لخيط جديد بالدخول. تحتفظ الخيط الأولى التي تتحقق من هذا الشرط بالرمز وتنتقل إلى العداد بالترتيب.
- الحالة الثانية هي عندما لا يكون الخيط الذي يحمل الرمز هو الذي يرسل إشارة إلى الخيوط التي تنتظر الرمز بأنه متاح. ومع ذلك، يجب عليه تحريره، لأن الخيط المسؤول عن إرسال هذه الإشارة يجب أن يكون حامل الرمز. وسيقوم بذلك باستخدام العملية:
أصبح الرمز متاحًا الآن، ولكن الخيوط التي تنتظره (التي قامت بعملية Wait(token)) لم يتم إخطارها. يتم إسناد هذه المهمة إلى خيط آخر، والذي سيقوم في مرحلة ما بتنفيذ كود مشابه لما يلي:
في النهاية، فإن البنية القياسية المقترحة في فصل "Threading" من كتاب C# 3.0 هي كما يلي:
- define counter access token :
- طلب الوصول إلى العداد:
lock(jeton){
while (! jeNeSuisPasCeluiQuiEstAttendu)
Monitor.Wait(jeton);
}
// passage au guichet
...
يعادل
لاحظ أنه في هذا المخطط يتم تحرير الرمز على الفور، بمجرد تجاوز الحاجز. يمكن بعد ذلك لخيط آخر اختبار شرط الوصول. وبالتالي، فإن البنية السابقة تسمح لجميع الخيوط بالتحقق من شرط الوصول. إذا لم يكن هذا ما تريده، يمكنك كتابة:
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).
فيما يلي مثال على التنفيذ:
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 خيوط خدمة مئات الطلبات.
- نحن نؤمن التطبيق: من خلال تعيين عدد أقصى من الخيوط، نتجنب إرباك التطبيق بعدد كبير جدًا من الطلبات. سيتم وضع هذه الطلبات في قائمة انتظار الملفات.
لتعيين مهمة إلى مؤشر ترابط في المجموعة، استخدم إحدى الطريقتين التاليتين:
- ThreadPool.QueueWorkItem(WaitCallBack)
- 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-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.
والنتائج هي كما يلي:
10.9. البيانات المحلية للخيط
10.9.1. المبدأ
لنفترض وجود تطبيق من ثلاث طبقات:
![]() |
لنفترض أن التطبيق متعدد المستخدمين، مثل تطبيق ويب على سبيل المثال. يتم خدمة كل مستخدم بواسطة مؤشر ترابط مخصص. وتكون دورة حياة مؤشر الترابط كما يلي:
- يتم إنشاء الخيط أو طلبه من مجموعة الخيوط لتلبية طلب المستخدم
- إذا كان هذا الطلب يتطلب بيانات، فسيقوم الخيط بتنفيذ طريقة من طبقة [ui]، والتي ستستدعي طريقة من طبقة [metier]، والتي بدورها ستستدعي طريقة من طبقة [dao].
- يعيد الخيط الاستجابة إلى المستخدم. ثم يختفي أو يتم إعادة تدويره إلى مجموعة الخيوط.
في العملية 2، قد يكون من المثير للاهتمام أن يكون للخيط بياناته الخاصة، أي غير مشتركة مع الخيوط الأخرى. يمكن أن تنتمي هذه البيانات، على سبيل المثال، إلى المستخدم المعين الذي يخدمه الخيط. يمكن بعد ذلك استخدام هذه البيانات في الطبقات المختلفة [ui، metier، dao].
تسمح فئة Thread بهذا السيناريو بفضل نوع من القاموس الخاص الذي تكون مفاتيحه من نوع LocalDataStoreSlot:
ينشئ إدخالاً في القاموس الخاص للخيط باسم المفتاح. | |
يربط بيانات القيمة باسم المفتاح من القاموس الخاص للخيط | |
يسترد القيمة المرتبطة بالاسم من القاموس الخاص للخيط |
قد يكون نموذج الاستخدام كما يلي:
- لإنشاء زوج (مفتاح، قيمة) مرتبط بالخيط الحالي:
- لاسترداد القيمة المرتبطة بالمفتاح:
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: كما هو مذكور أعلاه، ولكن ضمن معاملة
النتائج هي كما يلي:
- السطران 5-6: أدى الإدراج خارج المعاملة إلى ترك العنصر الأول في قاعدة البيانات
- السطر 9: الإدراج في معاملة لم يترك أي عناصر في قاعدة البيانات
10.9.3. الخلاصة
أظهر المثال السابق مزايا البيانات المحلية للخيط (thread-local) في إدارة المعاملات. ولا ينبغي تكراره كما هو. تستخدم أطر العمل مثل Spring وNhibernate وغيرها هذه التقنية، لكنها تجعلها أكثر شفافية: فمن الممكن لطبقة [metier] استخدام المعاملات دون الحاجة إلى علم طبقة [dao]. لا توجد معاملات في كود طبقة [dao]. يتم تحقيق ذلك باستخدام تقنية وكيل تسمى AOP (البرمجة الموجهة للجوانب). مرة أخرى، نحثك على استخدام هذه الأطر.
10.10. لمعرفة المزيد...
للحصول على نظرة أكثر تعمقًا في مجال تزامن الخيوط الصعب، اقرأ الفصل "Threading" من كتاب C# 3.0 المشار إليه في مقدمة هذا المستند. فهو يعرض العديد من تقنيات التزامن لأنواع مختلفة من المواقف.







