8. خيوط التنفيذ
8.1. مقدمة
عند تشغيل تطبيق ما، يتم تنفيذه في تدفق تنفيذي يُسمى مؤشر ترابط. الفئة .NET التي تمثل thread هي الفئة System.Threading.Thread ولها التعريف التالي:

لن نستخدم سوى بعض خصائص وأساليب هذه الفئة:
تُرجع مؤشر الترابط الذي يتم تنفيذه حاليًا | |
اسم الخيط | |
يشير إلى ما إذا كان الخيط نشطًا (true) أم لا (false) | |
يبدأ تشغيل مؤشر الترابط | |
يوقف تنفيذ الخيط نهائيًا | |
يوقف تشغيل مؤشر ترابط لمدة n مللي ثانية | |
يعلق تنفيذ مؤشر الترابط مؤقتًا | |
يستأنف تنفيذ مؤشر ترابط معلق | |
عملية معطلة - تنتظر انتهاء الخيط للانتقال إلى التعليمات التالية |
لنلقِ نظرة على تطبيق أول يوضح وجود خيط تنفيذ رئيسي، وهو الخيط الذي يتم فيه تنفيذ الدالة Main الخاصة بفئة ما:
' استخدام مؤشرات الترابط
Imports System
Imports System.Threading
Public Module thread1
Public Sub Main()
' تهيئة مؤشر الترابط الحالي
Dim main As Thread = Thread.CurrentThread
' العرض
Console.Out.WriteLine(("Thread courant : " + main.Name))
' تغيير الاسم
main.Name = "main"
' التحقق
Console.Out.WriteLine(("Thread courant : " + main.Name))
' حلقة لا نهائية
While True
' العرض
Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
' إيقاف مؤقت
Thread.Sleep(1000)
End While
End Sub
End Module
نتائج العرض على الشاشة:
dos>thread1
Thread courant :
Thread courant : main
main : 06:13:55
main : 06:13:56
main : 06:13:57
main : 06:13:58
main : 06:13:59
يوضح المثال السابق النقاط التالية:
- يتم تنفيذ الدالة Main بشكل صحيح في مؤشر ترابط
- يمكن الوصول إلى خصائص هذا الخيط من خلال Thread.CurrentThread
- دور الأسلوب Sleep. هنا، يدخل الخيط الذي ينفذ Main في حالة سكون بانتظام لمدة ثانية واحدة بين كل عرضين.
8.2. إنشاء خيوط التنفيذ
من الممكن وجود تطبيقات تُنفَّذ فيها أجزاء من الكود بشكل «متزامن» في خيوط تنفيذ مختلفة. وعندما نقول إن threads تُنفَّذ بشكل متزامن، فإننا غالبًا ما نستخدم مصطلحًا غير دقيق. إذا كانت الآلة تحتوي على معالج واحد فقط، كما هو الحال في كثير من الأحيان، فإن عمليات thread تتشارك هذا المعالج: فكل منها تتحكم فيه، بالتناوب، لفترة قصيرة (بضعة ميلي ثوانٍ). وهذا ما يخلق الوهم بالتوازي في التنفيذ. يعتمد الوقت المخصص لكل عملية على عوامل متنوعة، من بينها أولويتها التي لها قيمة افتراضية، ولكن يمكن تحديدها أيضًا عن طريق البرمجة. وعندما يكون المعالج متاحًا لإحدى العمليات، فإنها تستخدمه عادةً طوال المدة المخصصة لها. ومع ذلك، يمكنها التخلي عنه قبل انتهاء المدة المحددة:
- بالانتظار لحدوث حدث ما (wait، join، suspend)
- بالانتقال إلى حالة السكون لفترة محددة (sleep)
- يتم إنشاء الخيط T أولاً بواسطة مُنشئه
ThreadStart هو من نوع delegate ويُعرِّف النموذج الأولي لدالة بدون معلمات:
فيما يلي نموذج تقليدي:
سيتم تنفيذ الدالة run التي تم تمريرها كمعلمة عند بدء تشغيل الخيط (Thread).
- يتم تشغيل مؤشر الترابط T بواسطة الدالة T.Start(): ثم يتم تنفيذ الدالة [run] التي تم تمريرها إلى منشئ T بواسطة الخيط T. ولا ينتظر البرنامج الذي ينفذ الأمر T.start() انتهاء المهمة T: بل ينتقل فورًا إلى الأمر التالي. وبذلك يكون لدينا مهمتان تُنفذان بالتوازي. وغالبًا ما يتعين عليهما التواصل فيما بينهما لمعرفة مدى التقدم المحرز في العمل المشترك المطلوب إنجازه. وهذه هي مشكلة تزامن الخيوط.
- بمجرد بدء تشغيله، يعمل الخيط بشكل مستقل. وسيتوقف عندما تنتهي الدالة start التي ينفذها من عملها.
- يمكن إرسال بعض الإشارات إلى المهمة T:
- تأمره الدالة T.Suspend() بالتوقف مؤقتًا
- T.Resume() تأمرها باستئناف عملها
- T.Abort() تأمرها بالتوقف نهائيًا
- يمكن أيضًا انتظار انتهاء تنفيذها باستخدام T.join(). هذه تعليمات مانعة: يتم تعليق البرنامج الذي ينفذها حتى تنتهي المهمة T من عملها. هذه طريقة للتزامن.
لنلقِ نظرة على البرنامج التالي:
' خيارات
Option Strict On
Option Explicit On
' مساحات الأسماء
Imports System
Imports System.Threading
Module thread2
Public Sub Main()
' تهيئة مؤشر الترابط الحالي
Dim main As Thread = Thread.CurrentThread
' تعيين اسم للخيط
main.Name = "main"
' إنشاء خيوط التنفيذ
Dim tâches(4) As Thread
Dim i As Integer
For i = 0 To tâches.Length - 1
' إنشاء الخيط i
tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
' تحديد اسم الخيط
tâches(i).Name = "tache_" & i
' بدء تشغيل الخيط i
tâches(i).Start()
Next i
' نهاية الروتين
Console.Out.WriteLine(("fin du thread " + main.Name))
End Sub
Public Sub affiche()
' عرض بداية التنفيذ
Console.Out.WriteLine(("Début d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
' التوقف مؤقتًا لمدة 1 ثانية
Thread.Sleep(1000)
' عرض نهاية التنفيذ
Console.Out.WriteLine(("Fin d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
End Sub
End Module
يُنشئ الخيط الرئيسي، وهو الذي يُنفذ الدالة Main، 5 خيوط أخرى مكلفة بتنفيذ الطريقة الثابتة affiche. وتكون النتائج كما يلي:
dos>thread2
fin du thread main
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:53
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:54
هذه النتائج مفيدة للغاية:
- نلاحظ أولاً أن بدء تنفيذ الخيط لا يؤدي إلى توقف العملية. فقد بدأت الطريقة Main تنفيذ 5 خيوط بالتوازي وأنهت تنفيذها قبلها. العملية
تبدأ تشغيل الخيط tâches[i]، ولكن بمجرد الانتهاء من ذلك، يستمر التنفيذ فورًا مع التعليمات التالية دون انتظار انتهاء تشغيل الخيط.
- يجب أن تنفذ جميع الخيوط التي تم إنشاؤها الطريقة affiche. ترتيب التنفيذ غير متوقع. حتى لو بدا في المثال أن ترتيب التنفيذ يتبع ترتيب طلبات التنفيذ، فلا يمكن استخلاص قواعد عامة من ذلك. يحتوي نظام التشغيل هنا على 6 خيوط ومعالج واحد. وسيقوم بتوزيع المعالج على هذه الخيوط الست وفقًا لقواعده الخاصة.
- ونلاحظ في النتائج نتيجة لتنفيذ الأسلوب Sleep. في المثال، الخيط 0 هو الذي ينفذ أولاً الأسلوب affiche. يتم عرض رسالة بدء التنفيذ، ثم يقوم بتنفيذ الطريقة Sleep التي تعلقه لمدة ثانية واحدة. عندئذ يفقد المعالج الذي يصبح متاحًا لخيط آخر. يوضح المثال أن الخيط 1 هو الذي سيحصل عليه. سيتبع الخيط 1 نفس المسار، وكذلك الخيوط الأخرى. وعندما تنتهي ثانية السكون للخيط 0، يمكن أن يستأنف تنفيذه. يمنحه النظام وحدة المعالجة المركزية، فيتمكن من إنهاء تنفيذ الأسلوب affiche.
لنعدل برنامجنا لإنهاء تنفيذ الأسلوب Main باستخدام التعليمات التالية:
يؤدي تنفيذ البرنامج الجديد إلى:
لا يتم تنفيذ الخيوط التي أنشأتها الدالة Main. السبب هو الأمر
هي المسؤولة عن ذلك: فهي تقوم بإزالة جميع خيوط التطبيق وليس فقط الخيط Main. الحل لهذه المشكلة هو أن تنتظر الطريقة Main انتهاء تنفيذ الخيوط التي أنشأتها قبل أن تنتهي هي نفسها. ويمكن القيام بذلك باستخدام الطريقة Join من الفئة Thread:
' الانتظار حتى انتهاء تنفيذ جميع الخيوط
For i = 0 To tâches.Length - 1
' في انتظار انتهاء تنفيذ الخيط i
tâches(i).Join()
Next i 'for
' نهاية الدالة
Console.Out.WriteLine(("fin du thread " + main.Name))
Environment.Exit(0)
ونحصل عندئذٍ على النتائج التالية:
Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:48
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:50
fin du thread main
8.3. أهمية الخيوط
الآن بعد أن أوضحنا وجود مؤشر ترابط افتراضي، وهو الذي ينفذ الأسلوب Main، وبعد أن عرفنا كيفية إنشاء مؤشرات ترابط أخرى، دعونا نتوقف عند فائدة مؤشرات الترابط بالنسبة لنا والسبب وراء عرضها هنا. هناك نوع من التطبيقات التي تتناسب جيدًا مع استخدام الخيوط، وهي تطبيقات العميل-الخادم على الإنترنت. في مثل هذا التطبيق، يستجيب خادم موجود على جهاز S1 لطلبات العملاء الموجودين على أجهزة بعيدة مثل C1، C2، ...، Cn.
![]() |
نستخدم يوميًا تطبيقات الإنترنت التي تتوافق مع هذا المخطط: خدمات الويب، والبريد الإلكتروني، وتصفح المنتديات، ونقل الملفات... في المخطط أعلاه، يجب أن يخدم الخادم S1 العملاء Ci في وقت واحد. إذا أخذنا مثالاً على خادم FTP (بروتوكول نقل الملفات) الذي يزود عملائه بالملفات، فإننا نعلم أن عملية نقل الملفات قد تستغرق أحيانًا عدة ساعات. وبالطبع، من المستحيل أن يحتكر عميل واحد الخادم بمفرده لمدة طويلة كهذه. والأمر المعتاد هو أن يقوم الخادم بإنشاء عدد من خيوط التنفيذ يساوي عدد العملاء. ثم يُكلف كل خيط بالتعامل مع عميل معين. ونظرًا لأن المعالج يتم تقاسمه دوريًّا بين جميع الخيوط النشطة على الجهاز، فإن الخادم يقضي وقتًا قصيرًا مع كل عميل، مما يضمن توفير الخدمة بشكل متزامن.
![]() |
8.4. الوصول إلى الموارد المشتركة
في مثال العميل-الخادم المذكور أعلاه، يخدم كل مؤشر ترابط عميلاً بشكل مستقل إلى حد كبير. ومع ذلك، قد يتعين على مؤشرات الترابط التعاون لتقديم الخدمة المطلوبة لعملائها، لا سيما للوصول إلى الموارد المشتركة. يُذكّر المخطط أعلاه بمكاتب خدمة إحدى الإدارات الكبيرة، مثل مكتب البريد على سبيل المثال، حيث يخدم موظف واحد عميلاً واحداً في كل مكتب. لنفترض أن هؤلاء الموظفين يُطلب منهم من حين لآخر عمل نسخ من المستندات التي يجلبها عملاؤهم، وأنه لا توجد سوى آلة تصوير واحدة. لا يمكن لموظفين اثنين استخدام آلة التصوير في نفس الوقت. إذا وجد الموظف i أن آلة التصوير مشغولة من قبل الموظف j، فسيضطر إلى الانتظار. يُطلق على هذه الحالة اسم «الوصول إلى مورد مشترك»، وهي مسألة صعبة الإدارة إلى حد ما في مجال علوم الحاسوب. لنأخذ المثال التالي:
- سيقوم أحد التطبيقات بإنشاء n خيوط، حيث يتم تمرير n كمعلمة
- المورد المشترك هو عداد يجب أن يزيد كل خيط يتم إنشاؤه من قيمته
- في نهاية التطبيق، يتم عرض قيمة العداد. لذا يجب أن تكون النتيجة n.
البرنامج هو كما يلي:
' خيارات
Option Explicit On
Option Strict On
' استخدام الخيوط
Imports System
Imports System.Threading
Public Class thread3
' متغيرات الفئة
Private Shared cptrThreads As Integer = 0
Public Overloads Shared Sub Main(ByVal args() As [String])
' دليل الاستخدام
Const syntaxe As String = "pg nbThreads"
Const nbMaxThreads As Integer = 100
' التحقق من عدد الوسيطات
If args.Length <> 1 Then
' خطأ
Console.Error.WriteLine(syntaxe)
' إيقاف
Environment.Exit(1)
End If
' التحقق من جودة الوسيطة
Dim nbThreads As Integer = 0
Try
nbThreads = Integer.Parse(args(0))
If nbThreads < 1 Or nbThreads > nbMaxThreads Then
Throw New Exception
End If
Catch
' خطأ
Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et " & nbMaxThreads & ")")
' نهاية
Environment.Exit(2)
End Try
' إنشاء وتوليد الخيوط
Dim threads(nbThreads - 1) As Thread
Dim i As Integer
For i = 0 To nbThreads - 1
' إنشاء
threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
' التسمية
threads(i).Name = "tache_" & i
' التشغيل
threads(i).Start()
Next i
' انتظار انتهاء الخيوط
For i = 0 To nbThreads - 1
threads(i).Join()
Next i ' affichage compteur
Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
End Sub
Public Shared Sub incrémente()
' زيادة عداد الخيوط
' قراءة العداد
Dim valeur As Integer = cptrThreads
' المتابعة
Console.Out.WriteLine(("A " + DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
' في انتظار
Thread.Sleep(1000)
' زيادة العداد
cptrThreads = valeur + 1
' المتابعة
Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
End Sub
End Class
لن نتطرق إلى الجزء المتعلق بإنشاء الخيوط الذي تمت دراسته سابقًا. بل سنركز على الدالة incrémente، التي يستخدمها كل خيط لزيادة قيمة العداد الثابت cptrThreads.
- يتم قراءة العداد
- يتوقف الخيط لمدة ثانية واحدة. وبالتالي يفقد وحدة المعالجة المركزية
- يتم زيادة العداد
الخطوة 2 موجودة فقط لإجبار الخيط على فقدان وحدة المعالجة المركزية. وسيتم منح هذه الوحدة لخيط آخر. في الواقع العملي، لا يوجد ما يضمن ألا يتم مقاطعة الخيط بين اللحظة التي يقرأ فيها العداد واللحظة التي يزيد فيها قيمته. يوجد خطر فقدان وحدة المعالجة المركزية بين لحظة قراءة قيمة العداد ولحظة كتابة قيمته بعد زيادتها بمقدار 1. في الواقع، ستخضع عملية الزيادة لعدة تعليمات أساسية على مستوى وحدة المعالجة المركزية والتي قد تتعرض للمقاطعة. لذا فإن الخطوة 2 المتمثلة في السكون لمدة ثانية واحدة موجودة فقط لتنظيم هذا الخطر. النتائج التي تم الحصول عليها هي كما يلي:
dos>thread3 5
A 05:44:34, le thread tache_0 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_1 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_2 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_3 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_4 a lu la valeur du compteur : 0
A 05:44:35, le thread tache_0 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_1 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_2 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_3 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_4 a écrit la valeur du compteur : 1
Nombre de threads générés : 1
عند قراءة هذه النتائج، يتضح لنا ما يحدث:
- يقوم مؤشر الترابط الأول بقراءة العداد. فيجد القيمة 0.
- يتوقف لمدة ثانية واحدة، وبالتالي يفقد وحدة المعالجة المركزية
- ثم يستحوذ مؤشر ترابط ثانٍ على المعالج ويقرأ هو الآخر قيمة العداد. ولا تزال القيمة عند 0 لأن مؤشر الترابط السابق لم يقم بزيادتها بعد. ويتوقف هو الآخر لمدة 1 ثانية.
- خلال ثانية واحدة، يتسنى للخيوط الخمسة جميعًا المرور وقراءة القيمة 0.
- وعندما تستيقظ الخيوط واحدة تلو الأخرى، ستقوم بزيادة القيمة 0 التي قرأتها وكتابة القيمة 1 في العداد، وهو ما يؤكده البرنامج الرئيسي (Main).
من أين تأتي المشكلة؟ لقد قرأ الخيط الثاني قيمة خاطئة لأن الخيط الأول قد تمت مقاطعته قبل أن ينهي مهمته المتمثلة في تحديث العداد في النافذة. وهذا يقودنا إلى مفهوم المورد الحرج والمقطع الحرج في البرنامج:
- المورد الحرج هو مورد لا يمكن أن يحتفظ به سوى خيط واحد في كل مرة. والمورد الحرج هنا هو العداد.
- المقطع الحرج في البرنامج هو تسلسل من التعليمات في تدفق تنفيذ الخيط الذي يصل خلاله إلى مورد حرج. يجب ضمان أن يكون الخيط هو الوحيد الذي يمكنه الوصول إلى المورد خلال هذا المقطع الحرج.
8.5. الوصول الحصري إلى مورد مشترك
في مثالنا، المقطع الحرج هو الكود الموجود بين قراءة العداد وكتابة قيمته الجديدة:
' قراءة العداد
Dim valeur As Integer = cptrThreads
' في انتظار
Thread.Sleep(1000)
' زيادة العداد
cptrThreads = valeur + 1
لتنفيذ هذا الكود، يجب ضمان أن يكون الخيط (thread) بمفرده. يمكن مقاطعته، ولكن أثناء هذه المقاطعة، يجب ألا يتمكن خيط آخر من تنفيذ نفس الكود. توفر منصة .NET عدة أدوات لضمان الدخول الفردي إلى الأجزاء الحرجة من الكود. سنستخدم فئة Mutex:

ولن نستخدم هنا سوى المنشئات والطرق التالية:
تنشئ كائن تزامن M | |
يطلب الخيط T1، الذي ينفذ العملية M.WaitOne()، ملكية كائن التزامن M. إذا لم يكن أي خيط يمتلك الميوتكس M (وهذا هو الحال في البداية)، فسيتم «تسليمه» إلى الخيط T1 الذي طلبه. وإذا قام خيط T2 بعد ذلك بقليل بتنفيذ العملية نفسها، فسيتم حظره. ففي الواقع، لا يمكن أن ينتمي الموتكس إلا إلى خيط واحد. وسيتم إلغاء حظره عندما يقوم الخيط T1 بتحرير الموتكس M الذي يحتفظ به. وبالتالي، يمكن أن يتم حظر عدة خيوط في انتظار الموتكس M. | |
الخيط T1 الذي ينفذ العملية M.ReleaseMutex() يتخلى عن ملكية الموتكس M.Lorsque، وسيفقد الخيط T1 وحدة المعالجة المركزية، وسيتمكن النظام من منحه لأحد الخيوط المنتظرة للموتكس M. وسيحصل عليه خيط واحد فقط بدوره، بينما ستظل الخيوط الأخرى المنتظرة للموتكس M معطلة |
يدير Mutex M الوصول إلى مورد مشترك R. يطلب مؤشر الترابط المورد R عبر M.WaitOne() ويعيده عبر M.ReleaseMutex(). ويُعد الجزء الحرج من الكود الذي يجب ألا يتم تنفيذه إلا بواسطة مؤشر ترابط واحد في كل مرة موردًا مشتركًا. يمكن مزامنة تنفيذ المقطع الحرج على النحو التالي:
حيث M هو كائن Mutex. وبالطبع يجب ألا ننسى أبدًا تحرير كائن Mutex الذي أصبح غير ضروري حتى يتمكن مؤشر ترابط آخر من الدخول إلى القسم الحرج، وإلا فإن مؤشرات الترابط التي تنتظر موتكس لم يتم تحريره أبدًا لن تتمكن أبدًا من الوصول إلى المعالج. من ناحية أخرى، يجب تجنب حالة التعطل المتبادل (deadlock) التي ينتظر فيها خيطان بعضهما البعض. لنفكر في الإجراءات التالية التي تتعاقب زمنياً:
- يستحوذ مؤشر الترابط T1 على ملكية موتكس M1 للوصول إلى مورد مشترك R1
- يحصل الخيط T2 على ملكية موتكس M2 للوصول إلى مورد مشترك R2
- يطلب الخيط T1 الحصول على الموتكس M2. ويتم حظره.
- يطلب الخيط T2 الحصول على الموتكس M1. وهو محجوب.
هنا، ينتظر الخيطان T1 و T2 بعضهما البعض. تظهر هذه الحالة عندما تحتاج الخيوط إلى موردين مشتركين، المورد R1 الذي يتحكم فيه الموتكس M1 والمورد R2 الذي يتحكم فيه الموتكس M2. أحد الحلول الممكنة هو طلب الموردين في آن واحد باستخدام موتكس واحد M. لكن هذا ليس ممكنًا دائمًا إذا كان ذلك يؤدي، على سبيل المثال، إلى احتجاز مورد مكلف لفترة طويلة. وهناك حل آخر يتمثل في أن يقوم مؤشر الترابط الذي يمتلك المورد M1 ولا يستطيع الحصول على المورد M2، بإطلاق المورد M1 لتجنب التداخل. وإذا طبقنا ما رأيناه للتو على المثال السابق، فسيصبح تطبيقنا كما يلي:
' خيارات
Option Explicit On
Option Strict On
' استخدام الخيوط
Imports System
Imports System.Threading
Public Class thread4
' متغيرات الفئة
Private Shared cptrThreads As Integer = 0 ' compteur de threads
Private Shared autorisation As Mutex
Public Overloads Shared Sub Main(ByVal args() As [String])
' دليل الاستخدام
Const syntaxe As String = "pg nbThreads"
Const nbMaxThreads As Integer = 100
' التحقق من عدد الوسيطات
If args.Length <> 1 Then
' خطأ
Console.Error.WriteLine(syntaxe)
' إيقاف
Environment.Exit(1)
End If
' التحقق من جودة الوسيطة
Dim nbThreads As Integer = 0
Try
nbThreads = Integer.Parse(args(0))
If nbThreads < 1 Or nbThreads > nbMaxThreads Then
Throw New Exception
End If
Catch
End Try
' تهيئة إذن الوصول إلى قسم حرج
autorisation = New Mutex
' إنشاء وتوليد مؤشرات الترابط
Dim threads(nbThreads) As Thread
Dim i As Integer
For i = 0 To nbThreads - 1
' إنشاء
threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
' تسمية
threads(i).Name = "tache_" & i
' التشغيل
threads(i).Start()
Next i
' انتظار انتهاء الخيوط
For i = 0 To nbThreads - 1
threads(i).Join()
Next i
' عرض العداد
Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
End Sub
Public Shared Sub incrémente()
' زيادة عداد الخيوط
' طلب الإذن بدخول القسم الحرج
autorisation.WaitOne()
' قراءة العداد
Dim valeur As Integer = cptrThreads
' المتابعة
Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
' الانتظار
Thread.Sleep(1000)
' زيادة العداد
cptrThreads = valeur + 1
' المتابعة
Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
' منح إذن الوصول
autorisation.ReleaseMutex()
End Sub
End Class
النتائج التي تم الحصول عليها تتوافق مع المتوقع:
dos>thread4 5
A 05:51:10, le thread tache_0 a lu la valeur du compteur : 0
A 05:51:11, le thread tache_0 a écrit la valeur du compteur : 1
A 05:51:11, le thread tache_1 a lu la valeur du compteur : 1
A 05:51:12, le thread tache_1 a écrit la valeur du compteur : 2
A 05:51:12, le thread tache_2 a lu la valeur du compteur : 2
A 05:51:13, le thread tache_2 a écrit la valeur du compteur : 3
A 05:51:13, le thread tache_3 a lu la valeur du compteur : 3
A 05:51:14, le thread tache_3 a écrit la valeur du compteur : 4
A 05:51:14, le thread tache_4 a lu la valeur du compteur : 4
A 05:51:15, le thread tache_4 a écrit la valeur du compteur : 5
Nombre de threads générés : 5
8.6. التزامن حسب الأحداث
لنتأمل الحالة التالية، التي تُسمى أحيانًا حالة المنتجين والمستهلكين.
- لدينا جدول تقوم فيه بعض العمليات بإدخال البيانات (المنتجون) بينما تقوم عمليات أخرى بقراءتها (المستهلكون).
- المنتجون متساوون فيما بينهم ولكنهم حصريون: لا يمكن إلا لمنتج واحد في كل مرة أن يودع بياناته في المصفوفة.
- المستهلكون متساوون فيما بينهم ولكنهم متناوبون: لا يمكن إلا لقارئ واحد في كل مرة قراءة البيانات المخزنة في الجدول.
- لا يمكن للمستهلك قراءة البيانات من الجدول إلا عندما يقوم منتج بإيداعها فيه، ولا يمكن للمنتج إيداع بيانات جديدة في الجدول إلا عندما يتم استهلاك البيانات الموجودة فيه.
يمكن في هذا العرض التمييز بين موردين مشتركين:
- الجدول القابل للكتابة
- المصفوفة للقراءة
يمكن التحكم في الوصول إلى هذين الموردين المشتركين بواسطة Mutex كما رأينا سابقًا، واحد لكل مورد. بمجرد حصول المستهلك على الجدول للقراءة، يجب عليه التحقق من وجود بيانات فيه بالفعل. سنستخدم حدثًا لإعلامه بذلك. وبالمثل، يجب على المنتج الذي حصل على الجدول للكتابة أن ينتظر حتى يقوم مستهلك بإفراغه. سنستخدم حدثًا هنا أيضًا.
وستكون الأحداث المستخدمة جزءًا من الفئة AutoResetEvent:

هذا النوع من الأحداث مشابه للقيمة المنطقية (Boolean) ولكنه يتجنب عمليات الانتظار النشطة أو شبه النشطة. وبالتالي، إذا كان حق الكتابة يتم التحكم فيه بواسطة قيمة منطقية peutEcrire، فسيقوم المنتج قبل الكتابة بتنفيذ كود من النوع التالي:
أو
في الطريقة الأولى، يستهلك الخيط موارد المعالج دون داعٍ. أما في الطريقة الثانية، فيتحقق الخيط من حالة المتغير المنطقي peutEcrire كل 100 مللي ثانية. تتيح الفئة AutoResetEvent تحسين الأداء أكثر: سيطلب الخيط إيقاظه عند حدوث الحدث الذي ينتظره:
AutoEvent peutEcrire=new AutoResetEvent(false) ' peutEcrire=false;
....
peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai
العملية
تقوم بتعيين القيمة المنطقية peutEcrire إلى false. العملية
التي ينفذها مؤشر ترابط ما تجعل هذا المؤشر يمر إذا كانت القيمة المنطقية peutEcrire صحيحة، وإلا فإنه يتم حظره حتى تصبح صحيحة. وسيقوم مؤشر ترابط آخر بتغيير قيمته إلى «صحيح» من خلال العملية peutEcrire.Set() أو إلى «خطأ» من خلال العملية peutEcrire.Reset().
برنامج المنتجين-المستهلكين هو كما يلي:
' استخدام خيوط القراءة والكتابة
' يوضح الاستخدام المتزامن للموارد المشتركة والتزامن
' خيارات
Option Explicit On
Option Strict On
' استخدام الخيوط
Imports System
Imports System.Threading
Public Class lececr
' متغيرات الفئة
Private Shared data(5) As Integer ' ressource partagée entre threads lecteur et threads écrivain
Private Shared lecteur As Mutex ' variable de synchronisation pour lire le tableau
Private Shared écrivain As Mutex ' variable de synchronisation pour écrire dans le tableau
Private Shared objRandom As New Random(DateTime.Now.Second) ' un générateur de nombres aléatoires
Private Shared peutLire As AutoResetEvent ' signale qu'on peut lire le contenu de data
Private Shared peutEcrire As AutoResetEvent
Public Shared Sub Main(ByVal args() As [String])
' عدد الخيوط المطلوب إنشاؤها
Const nbThreads As Integer = 3
' تهيئة العلامات
peutLire = New AutoResetEvent(False) ' on ne peut pas encore lire
peutEcrire = New AutoResetEvent(True) ' on peut déjà écrire
' تهيئة متغيرات التزامن
lecteur = New Mutex ' synchronise les lecteurs
écrivain = New Mutex ' synchronise les écrivains
' إنشاء خيوط القراءة
Dim lecteurs(nbThreads) As Thread
Dim i As Integer
For i = 0 To nbThreads - 1
' إنشاء
lecteurs(i) = New Thread(New ThreadStart(AddressOf lire))
lecteurs(i).Name = "lecteur_" & i
' تشغيل
lecteurs(i).Start()
Next i
' إنشاء خيوط الكتابة
Dim écrivains(nbThreads) As Thread
For i = 0 To nbThreads - 1
' إنشاء
écrivains(i) = New Thread(New ThreadStart(AddressOf écrire))
écrivains(i).Name = "écrivain_" & i
' التشغيل
écrivains(i).Start()
Next i
' نهاية اليد
Console.Out.WriteLine("fin de Main...")
End Sub
' قراءة محتوى الجدول
Public Shared Sub lire()
' القسم الحرج
lecteur.WaitOne() ' un seul lecteur peut passer
peutLire.WaitOne() ' on doit pouvoir lire
' قراءة الجدول
Dim i As Integer
For i = 0 To data.Length - 1
'الانتظار لمدة 1 ثانية
Thread.Sleep(1000)
' العرض
Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
Next i
' لم يعد بالإمكان القراءة
peutLire.Reset()
' يمكن الكتابة
peutEcrire.Set()
' نهاية القسم الحرج
lecteur.ReleaseMutex()
End Sub
' الكتابة في الجدول
Public Shared Sub écrire()
' قسم حرج
' يمكن لكاتب واحد فقط المرور
écrivain.WaitOne()
' يجب انتظار إذن الكتابة
peutEcrire.WaitOne()
' كتابة في الجدول
Dim i As Integer
For i = 0 To data.Length - 1
'الانتظار لمدة 1 ثانية
Thread.Sleep(1000)
' العرض
data(i) = objRandom.Next(0, 1000)
Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : L'écrivain " & Thread.CurrentThread.Name & " a écrit le nombre " & data(i)))
Next i
' لم يعد بالإمكان الكتابة
peutEcrire.Reset()
' يمكن القراءة
peutLire.Set()
'نهاية القسم الحرج
écrivain.ReleaseMutex()
End Sub
End Class
يُنتج التنفيذ النتائج التالية:
dos>lececr
fin de Main...
05:56:56 : L'écrivain écrivain_0 a écrit le nombre 459
05:56:57 : L'écrivain écrivain_0 a écrit le nombre 955
05:56:58 : L'écrivain écrivain_0 a écrit le nombre 212
05:56:59 : L'écrivain écrivain_0 a écrit le nombre 297
05:57:00 : L'écrivain écrivain_0 a écrit le nombre 37
05:57:01 : L'écrivain écrivain_0 a écrit le nombre 623
05:57:02 : Le lecteur lecteur_0 a lu le nombre 459
05:57:03 : Le lecteur lecteur_0 a lu le nombre 955
05:57:04 : Le lecteur lecteur_0 a lu le nombre 212
05:57:05 : Le lecteur lecteur_0 a lu le nombre 297
05:57:06 : Le lecteur lecteur_0 a lu le nombre 37
05:57:07 : Le lecteur lecteur_0 a lu le nombre 623
05:57:08 : L'écrivain écrivain_1 a écrit le nombre 549
05:57:09 : L'écrivain écrivain_1 a écrit le nombre 34
05:57:10 : L'écrivain écrivain_1 a écrit le nombre 781
05:57:11 : L'écrivain écrivain_1 a écrit le nombre 555
05:57:12 : L'écrivain écrivain_1 a écrit le nombre 812
05:57:13 : L'écrivain écrivain_1 a écrit le nombre 406
05:57:14 : Le lecteur lecteur_1 a lu le nombre 549
05:57:15 : Le lecteur lecteur_1 a lu le nombre 34
05:57:16 : Le lecteur lecteur_1 a lu le nombre 781
05:57:17 : Le lecteur lecteur_1 a lu le nombre 555
05:57:18 : Le lecteur lecteur_1 a lu le nombre 812
05:57:19 : Le lecteur lecteur_1 a lu le nombre 406
05:57:20 : L'écrivain écrivain_2 a écrit le nombre 442
05:57:21 : L'écrivain écrivain_2 a écrit le nombre 83
^C
يمكن ملاحظة النقاط التالية:
- يوجد بالفعل قارئ واحد فقط في كل مرة، على الرغم من أن هذا القارئ يفقد وحدة المعالجة المركزية في القسم الحرج lire
- يوجد بالفعل كاتب واحد فقط في كل مرة على الرغم من أن هذا الكاتب يفقد وحدة المعالجة المركزية في القسم الحرج écrire
- لا يقرأ القارئ إلا عندما يكون هناك شيء لقراءته في المصفوفة
- لا يقوم الكاتب بالكتابة إلا بعد قراءة المصفوفة بالكامل

