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

سنستخدم فقط بعض خصائص وأساليب هذه الفئة:
تُرجع مؤشر الخيط الذي يعمل حاليًا | |
اسم الخيط | |
يشير إلى ما إذا كان الخيط نشطًا (صحيح) أم لا (خطأ) | |
يبدأ تنفيذ مؤشر الترابط | |
توقف تنفيذ مؤشر الترابط بشكل دائم | |
يوقف مؤقتًا مؤشر ترابط لمدة n مللي ثانية | |
تعلق مؤقتًا تنفيذ مؤشر ترابط | |
يستأنف تنفيذ مؤشر ترابط تم تعليقه | |
عملية حجب - تنتظر انتهاء الخيط قبل الانتقال إلى التعليمات التالية |
لنلقِ نظرة على تطبيق بسيط يوضح وجود مؤشر ترابط التنفيذ الرئيسي، وهو المؤشر الذي تعمل فيه دالة Main الخاصة بالفئة:
' use of threads
Imports System
Imports System.Threading
Public Module thread1
Public Sub Main()
' init current thread
Dim main As Thread = Thread.CurrentThread
' display
Console.Out.WriteLine(("Thread courant : " + main.Name))
' we change the name
main.Name = "main"
' check
Console.Out.WriteLine(("Thread courant : " + main.Name))
' infinite loop
While True
' display
Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
' temporary shutdown
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. إنشاء خيوط التنفيذ
من الممكن أن تكون هناك تطبيقات يتم فيها تنفيذ أجزاء من الكود "بشكل متزامن" في خيوط تنفيذ مختلفة. عندما نقول أن الخيوط تعمل بشكل متزامن، فإننا غالبًا ما نستخدم هذا المصطلح بشكل عام. إذا كان الجهاز يحتوي على معالج واحد فقط، كما هو الحال في كثير من الأحيان، فإن الخيوط تتشارك هذا المعالج: حيث يمكن لكل منها الوصول إليه، بالتناوب، لفترة وجيزة (بضع ميلي ثوانٍ). وهذا ما يخلق الوهم بالتنفيذ المتوازي. يعتمد مقدار الوقت المخصص لخيط على عوامل مختلفة، بما في ذلك أولويته، التي لها قيمة افتراضية ولكن يمكن أيضًا تعيينها برمجيًا. عندما يكون الخيط هو صاحب المعالج، فإنه عادةً ما يستخدمه طوال الوقت المخصص له. ومع ذلك، يمكنه تحريره مبكرًا:
- عن طريق انتظار حدث (wait، join، suspend)
- عن طريق السكون لفترة محددة (sleep)
- يتم إنشاء الخيط T أولاً بواسطة منشئه
ThreadStart هو من نوع delegate ويحدد النموذج الأولي لدالة بدون معلمات:
فيما يلي بنية نموذجية:
سيتم تنفيذ الدالة run التي تم تمريرها كمعلمة عند تشغيل الخيط.
- يتم بدء تنفيذ الخيط T بواسطة T.Start(): ثم يتم تنفيذ الدالة [run] التي تم تمريرها إلى منشئ T بواسطة الخيط T. لا ينتظر البرنامج الذي ينفذ عبارة T.Start() انتهاء المهمة T: بل ينتقل فورًا إلى العبارة التالية. لدينا الآن مهمتان تعملان بالتوازي. وغالبًا ما تحتاجان إلى التواصل مع بعضهما البعض لمعرفة حالة العمل المشترك الذي يتعين القيام به. هذه هي مشكلة تزامن الخيوط.
- بمجرد تشغيله، يعمل الخيط بشكل مستقل. سيتوقف عندما تنتهي وظيفة البدء التي ينفذها من عملها.
- يمكننا إرسال إشارات معينة إلى المهمة T:
- T.Suspend() يطلب منه التوقف مؤقتًا
- T.Resume() تأمره باستئناف عمله
- T.Abort() تأمره بالتوقف نهائيًا
- يمكنك أيضًا انتظار انتهاء تنفيذها باستخدام T.join(). هذه تعليمات حجب: يتم حجب البرنامج الذي ينفذها حتى تنتهي المهمة T من عملها. وهي وسيلة للتزامن.
دعونا نفحص البرنامج التالي:
' options
Option Strict On
Option Explicit On
' namespaces
Imports System
Imports System.Threading
Module thread2
Public Sub Main()
' init Current thread
Dim main As Thread = Thread.CurrentThread
' name the Thread
main.Name = "main"
' creation of execution threads
Dim tâches(4) As Thread
Dim i As Integer
For i = 0 To tâches.Length - 1
' create thread i
tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
' set the thread name
tâches(i).Name = "tache_" & i
' start execution of thread i
tâches(i).Start()
Next i
' end of hand
Console.Out.WriteLine(("fin du thread " + main.Name))
End Sub
Public Sub affiche()
' display start of execution
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")))
' sleep for 1 s
Thread.Sleep(1000)
' display end of run
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 خيوط أخرى مسؤولة عن تنفيذ عرض الدالة الثابتة. والنتائج هي كما يلي:
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 خيوط بالتوازي وانتهت من التنفيذ قبلها.
يبدأ تنفيذ مؤشر الترابط tasks[i]، ولكن بمجرد الانتهاء من ذلك، يستمر التنفيذ فورًا مع العبارة التالية دون انتظار انتهاء مؤشر الترابط.
- يجب أن تنفذ جميع الخيوط التي تم إنشاؤها طريقة العرض. ترتيب التنفيذ غير متوقع. على الرغم من أن ترتيب التنفيذ في المثال يبدو أنه يتبع ترتيب طلبات التنفيذ، لا يمكن استخلاص أي استنتاجات عامة من ذلك. يحتوي نظام التشغيل هنا على 6 خيوط ومعالج واحد. وسيقوم بتخصيص المعالج لهذه الخيوط الست وفقًا لقواعده الخاصة.
- تُظهر النتائج تأثير طريقة Sleep. في المثال، يكون الخيط 0 هو أول من ينفذ طريقة العرض. يتم عرض رسالة بدء التنفيذ، ثم ينفذ طريقة Sleep، التي تعلقه لمدة ثانية واحدة. ثم يفقد المعالج، الذي يصبح متاحًا لخيط آخر. يوضح المثال أن الخيط 1 سيحصل عليه. سيتبع الخيط 1 نفس المسار، وكذلك الخيوط الأخرى. عندما تنتهي فترة السكون التي تبلغ ثانية واحدة للخيط 0، يمكن استئناف تنفيذه. يمنحه النظام المعالج، ويمكنه إكمال تنفيذ طريقة العرض.
دعونا نعدل برنامجنا لإنهاء طريقة Main بالتعليمات التالية:
يؤدي تشغيل البرنامج الجديد إلى:
لا يتم تنفيذ الخيوط التي أنشأتها الدالة Main. إنها العبارة
هي التي تقوم بذلك: فهي تنهي جميع الخيوط في التطبيق، وليس فقط الخيط الرئيسي. الحل لهذه المشكلة هو أن تنتظر الطريقة Main حتى تنتهي الخيوط التي أنشأتها من التنفيذ قبل إنهاء نفسها. يمكن القيام بذلك باستخدام الطريقة Join لفئة Thread:
' on attend la fin d'exécution de tous les threads
For i = 0 To tâches.Length - 1
' attente de la fin d'exécution du thread i
tâches(i).Join()
Next i 'for
' fin de main
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 العملاء C1، C2، ...، Cn في وقت واحد. إذا أخذنا مثال خادم FTP (بروتوكول نقل الملفات) الذي يوصل الملفات إلى عملائه، فإننا نعلم أن نقل الملفات قد يستغرق أحيانًا عدة ساعات. وبالطبع، من المستحيل أن يحتكر عميل واحد الخادم لفترة طويلة كهذه. ما يحدث عادةً هو أن الخادم ينشئ عددًا من خيوط التنفيذ يساوي عدد العملاء. ثم يكون كل خيط مسؤولاً عن التعامل مع عميل معين. ونظرًا لأن المعالج يتم مشاركته بشكل دوري بين جميع الخيوط النشطة على الجهاز، فإن الخادم يقضي وقتًا قصيرًا مع كل عميل، مما يضمن تزامن الخدمة.
![]() |
8.4. الوصول إلى الموارد المشتركة
في مثال العميل-الخادم المذكور أعلاه، يخدم كل مؤشر ترابط عميلاً بشكل مستقل إلى حد كبير. ومع ذلك، قد تحتاج مؤشرات الترابط إلى التعاون لتقديم الخدمة المطلوبة لعملائها، خاصة عند الوصول إلى الموارد المشتركة. يشبه الرسم البياني أعلاه مكاتب الاستقبال في مكتب حكومي كبير، مثل مكتب البريد، حيث يخدم موظف في كل مكتب عميلاً. لنفترض أن هؤلاء الموظفين يحتاجون من وقت لآخر إلى عمل نسخ من المستندات التي يجلبها عملاؤهم وأن هناك آلة تصوير واحدة فقط. لا يمكن لموظفين اثنين استخدام آلة التصوير في نفس الوقت. إذا وجد الموظف i أن آلة التصوير قيد الاستخدام من قبل الموظف j، فسيتعين عليه الانتظار. تسمى هذه الحالة الوصول إلى مورد مشترك، وفي علم الحاسوب، يعد إدارتها أمرًا صعبًا للغاية. انظر المثال التالي:
- سيقوم أحد التطبيقات بإنشاء n خيوط، حيث يتم تمرير n كمعلمة
- المورد المشترك هو عداد يجب زيادته بواسطة كل مؤشر تسلسل تم إنشاؤه
- في نهاية التطبيق، يتم عرض قيمة العداد. لذلك يجب أن نحصل على n.
البرنامج كما يلي:
' options
Option Explicit On
Option Strict On
' use of threads
Imports System
Imports System.Threading
Public Class thread3
' class variables
Private Shared cptrThreads As Integer = 0
Public Overloads Shared Sub Main(ByVal args() As [String])
' instructions for use
Const syntaxe As String = "pg nbThreads"
Const nbMaxThreads As Integer = 100
' verification no. of arguments
If args.Length <> 1 Then
' error
Console.Error.WriteLine(syntaxe)
' stop
Environment.Exit(1)
End If
' argument quality check
Dim nbThreads As Integer = 0
Try
nbThreads = Integer.Parse(args(0))
If nbThreads < 1 Or nbThreads > nbMaxThreads Then
Throw New Exception
End If
Catch
' error
Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et " & nbMaxThreads & ")")
' end
Environment.Exit(2)
End Try
' thread creation and generation
Dim threads(nbThreads - 1) As Thread
Dim i As Integer
For i = 0 To nbThreads - 1
' creation
threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
' naming
threads(i).Name = "tache_" & i
' launch
threads(i).Start()
Next i
' waiting for threads to finish
For i = 0 To nbThreads - 1
threads(i).Join()
Next i ' counter display
Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
End Sub
Public Shared Sub incrémente()
' increases thread counter
' meter reading
Dim valeur As Integer = cptrThreads
' follow-up
Console.Out.WriteLine(("A " + DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
' waiting
Thread.Sleep(1000)
' counter incrementation
cptrThreads = valeur + 1
' follow-up
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
لن نتطرق إلى جزء إنشاء الخيط، الذي تناولناه بالفعل. بدلاً من ذلك، دعونا نركز على طريقة Increment، التي يستخدمها كل خيط لزيادة العداد الثابت 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.
- يتوقف مؤقتًا لمدة ثانية واحدة، مما يفسح المجال للوحدة المركزية للمعالجة (CPU)
- ثم يستحوذ مؤشر ترابط ثانٍ على وحدة المعالجة المركزية ويقرأ أيضًا قيمة العداد. لا تزال القيمة 0 لأن مؤشر الترابط السابق لم يقم بزيادتها بعد. كما يتوقف مؤقتًا لمدة ثانية واحدة.
- في غضون ثانية واحدة، يكون لدى جميع الخيوط الخمسة الوقت الكافي للتشغيل وقراءة القيمة 0.
- وعندما تستيقظ الخيوط واحدًا تلو الآخر، ستزيد القيمة 0 التي قرأتها وتكتب القيمة 1 في العداد، وهو ما يؤكده البرنامج الرئيسي (Main).
من أين تأتي المشكلة؟ قرأ الخيط الثاني قيمة غير صحيحة لأن الخيط الأول تمت مقاطعته قبل أن ينهي مهمته، وهي تحديث العداد في النافذة. وهذا يقودنا إلى مفهوم الموارد الحرجة والأقسام الحرجة في البرنامج:
- المورد الحرج هو مورد لا يمكن أن يحتفظ به سوى مؤشر ترابط واحد في كل مرة. هنا، المورد الحرج هو العداد.
- القسم الحرج في البرنامج هو سلسلة من التعليمات في تدفق تنفيذ الخيط الذي يصل خلاله إلى المورد الحرج. يجب أن نتأكد من أنه خلال هذا القسم الحرج، يكون هو الوحيد الذي يمكنه الوصول إلى المورد.
8.5. الوصول الحصري إلى مورد مشترك
في مثالنا، القسم الحرج هو الكود الموجود بين قراءة العداد وكتابة قيمته الجديدة:
' meter reading
Dim valeur As Integer = cptrThreads
' waiting
Thread.Sleep(1000)
' counter incrementation
cptrThreads = valeur + 1
لتنفيذ هذا الرمز، يجب ضمان أن يكون الخيط بمفرده. قد يتم مقاطعته، ولكن أثناء تلك المقاطعة، يجب ألا يتمكن أي خيط آخر من تنفيذ هذا الرمز نفسه. توفر منصة .NET عدة أدوات لضمان الدخول أحادي الخيط إلى الأقسام الحرجة من الرمز. سنستخدم فئة Mutex:

هنا، سنستخدم فقط المنشئات والأساليب التالية:
ينشئ كائن تزامن M | |
يطلب الخيط T1، الذي ينفذ عملية M.WaitOne()، ملكية كائن التزامن M. إذا لم يكن Mutex M محتفظًا به أي خيط (الحالة الأولية)، يتم "منحه" للخيط T1، الذي طلبه. إذا قام الخيط T2، بعد ذلك بقليل، بتنفيذ نفس العملية، فسيتم حظره. في الواقع، لا يمكن أن ينتمي الموتكس إلا إلى خيط واحد فقط. وسيتم تحريره عندما يقوم الخيط T1 بتحرير الموتكس M الذي يحتفظ به. وبالتالي، يمكن حظر عدة خيوط أثناء انتظار الموتكس M. | |
يتخلى الخيط T1 الذي يقوم بالعملية M.ReleaseMutex() عن ملكية الموتكس M. عندما يفقد الخيط T1 المعالج، يمكن للنظام تخصيصه لأحد الخيوط التي تنتظر الموتكس M. سيحصل عليه واحد فقط بدوره؛ بينما تظل الخيوط الأخرى التي تنتظر M محجوبة |
يدير الموتكس M الوصول إلى المورد المشترك R. يطلب الخيط المورد R عبر M.WaitOne() ويحرره عبر M.ReleaseMutex(). يعد الجزء الحرج من الكود الذي يجب أن ينفذه خيط واحد فقط في كل مرة موردًا مشتركًا. يمكن تحقيق تزامن تنفيذ الجزء الحرج على النحو التالي:
حيث M هو كائن Mutex. بالطبع، يجب ألا تنسى أبدًا تحرير Mutex الذي لم يعد مطلوبًا حتى يتمكن مؤشر ترابط آخر من الدخول إلى القسم الحرج؛ وإلا، فإن مؤشرات الترابط التي تنتظر Mutex الذي لم يتم تحريره أبدًا لن تتمكن أبدًا من الوصول إلى المعالج. علاوة على ذلك، يجب تجنب حالة التعطل التي ينتظر فيها مؤشرا ترابط بعضهما البعض. ضع في اعتبارك الإجراءات التالية التي تحدث بالتسلسل:
- يستحوذ الخيط 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 لتجنب حالة التعطل. إذا طبقنا ما رأيناه للتو على المثال السابق، يصبح تطبيقنا كما يلي:
' options
Option Explicit On
Option Strict On
' use of threads
Imports System
Imports System.Threading
Public Class thread4
' class variables
Private Shared cptrThreads As Integer = 0 ' thread counter
Private Shared autorisation As Mutex
Public Overloads Shared Sub Main(ByVal args() As [String])
' instructions for use
Const syntaxe As String = "pg nbThreads"
Const nbMaxThreads As Integer = 100
' verification no. of arguments
If args.Length <> 1 Then
' error
Console.Error.WriteLine(syntaxe)
' stop
Environment.Exit(1)
End If
' argument quality check
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
' initialize access authorization to a critical section
autorisation = New Mutex
' thread creation and generation
Dim threads(nbThreads) As Thread
Dim i As Integer
For i = 0 To nbThreads - 1
' creation
threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
' naming
threads(i).Name = "tache_" & i
' launch
threads(i).Start()
Next i
' waiting for threads to finish
For i = 0 To nbThreads - 1
threads(i).Join()
Next i
' counter display
Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
End Sub
Public Shared Sub incrémente()
' increases thread counter
' we request permission to enter the critical secton
autorisation.WaitOne()
' meter reading
Dim valeur As Integer = cptrThreads
' follow-up
Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
' waiting
Thread.Sleep(1000)
' counter incrementation
cptrThreads = valeur + 1
' follow-up
Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
' access authorization is returned
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. التزامن القائم على الأحداث
لننظر إلى الموقف التالي، الذي يُشار إليه أحيانًا باسم سيناريو المنتج والمستهلك.
- لدينا مصفوفة تودع فيها بعض العمليات البيانات (المنتجون) وتقرأها عمليات أخرى (المستهلكون).
- المنتجون متساوون فيما بينهم ولكنهم متنافسون: لا يمكن إلا لمنتج واحد في كل مرة إيداع البيانات في المصفوفة.
- المستهلكون متساوون مع بعضهم البعض ولكنهم متنافيون: يمكن لقارئ واحد فقط في كل مرة قراءة البيانات المخزنة في المصفوفة.
- لا يمكن للمستهلك قراءة البيانات من الجدول إلا بعد أن يكتبها المنتج فيه، ولا يمكن للمنتج كتابة بيانات جديدة في الجدول إلا بعد استهلاك البيانات الموجودة.
في هذا الشرح، يمكننا التمييز بين موردين مشتركين:
- الجدول القابل للكتابة
- المصفوفة للقراءة فقط
يمكن التحكم في الوصول إلى هذين الموردين المشتركين بواسطة الموتكسات، كما رأينا سابقًا، واحد لكل مورد. بمجرد حصول المستهلك على المصفوفة للقراءة فقط، يجب عليه التحقق من وجود بيانات فيها بالفعل. سيتم استخدام حدث لإعلامه بذلك. وبالمثل، يجب على المنتج الذي حصل على المصفوفة للكتابة فقط الانتظار حتى يقوم المستهلك بإفراغها. سيتم استخدام حدث هنا أيضًا.
ستكون الأحداث المستخدمة من فئة AutoResetEvent:

هذا النوع من الأحداث مشابه للقيمة المنطقية (boolean) ولكنه يتجنب عمليات الانتظار النشطة أو شبه النشطة. وبالتالي، إذا تم التحكم في الوصول للكتابة بواسطة القيمة المنطقية *canWrite*، فسيقوم المنتج بتنفيذ كود مثل التالي قبل الكتابة:
أو
في الطريقة الأولى، يقوم الخيط بتعطيل المعالج دون داعٍ. وفي الطريقة الثانية، يتحقق من حالة المتغير المنطقي canWrite كل 100 مللي ثانية. تسمح فئة AutoResetEvent بمزيد من التحسين: سيطلب الخيط إيقاظه عند حدوث الحدث الذي ينتظره:
AutoEvent peutEcrire=new AutoResetEvent(false) ' peutEcrire=false;
....
peutEcrire.WaitOne() ' thread waits for peutEcrire event to change to true
العملية
تقوم بتهيئة المتغير المنطقي canWrite إلى false. العملية
التي ينفذها مؤشر ترابط ما تؤدي إلى استمرار ذلك المؤشر في العمل إذا كانت القيمة المنطقية canWrite* صحيحة؛ وإلا، يتم حظره حتى تصبح صحيحة. وسيقوم مؤشر ترابط آخر بتعيينها إلى صحيح باستخدام العملية canWrite.Set() أو إلى خطأ باستخدام العملية canWrite.Reset()*.
برنامج المنتج والمستهلك هو كما يلي:
' use of reader and writer threads
' illustrates the simultaneous use of shared resources and synchronization
' options
Option Explicit On
Option Strict On
' use of threads
Imports System
Imports System.Threading
Public Class lececr
' class variables
Private Shared data(5) As Integer ' resource shared between reader and writer threads
Private Shared lecteur As Mutex ' synchronization variable to read the table
Private Shared écrivain As Mutex ' synchronization variable to write to the table
Private Shared objRandom As New Random(DateTime.Now.Second) ' a random number generator
Private Shared peutLire As AutoResetEvent ' indicates that you can read the contents of data
Private Shared peutEcrire As AutoResetEvent
Public Shared Sub Main(ByVal args() As [String])
' number of threads to generate
Const nbThreads As Integer = 3
' flag initialization
peutLire = New AutoResetEvent(False) ' cannot be read yet
peutEcrire = New AutoResetEvent(True) ' we can already write
' initialization of synchronization variables
lecteur = New Mutex ' synchronizes drives
écrivain = New Mutex ' synchronizes writers
' creation of reader threads
Dim lecteurs(nbThreads) As Thread
Dim i As Integer
For i = 0 To nbThreads - 1
' creation
lecteurs(i) = New Thread(New ThreadStart(AddressOf lire))
lecteurs(i).Name = "lecteur_" & i
' launch
lecteurs(i).Start()
Next i
' creating writer threads
Dim écrivains(nbThreads) As Thread
For i = 0 To nbThreads - 1
' creation
écrivains(i) = New Thread(New ThreadStart(AddressOf écrire))
écrivains(i).Name = "écrivain_" & i
' launch
écrivains(i).Start()
Next i
'end of hand
Console.Out.WriteLine("fin de Main...")
End Sub
' read the contents of the table
Public Shared Sub lire()
' review section
lecteur.WaitOne() ' a single reader can pass
peutLire.WaitOne() ' you must be able to read
' table reading
Dim i As Integer
For i = 0 To data.Length - 1
'wait 1 s
Thread.Sleep(1000)
' display
Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
Next i
' we can no longer read
peutLire.Reset()
' we can write
peutEcrire.Set()
' end of critical section
lecteur.ReleaseMutex()
End Sub
' write in the table
Public Shared Sub écrire()
' review section
' only one writer can pass
écrivain.WaitOne()
' we have to wait for write authorization
peutEcrire.WaitOne()
' writing table
Dim i As Integer
For i = 0 To data.Length - 1
'wait 1 s
Thread.Sleep(1000)
' display
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
' we can no longer write
peutEcrire.Reset()
' you can read
peutLire.Set()
'end of critical section
é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
يمكن ملاحظة النقاط التالية:
- يوجد بالفعل قارئ واحد فقط في كل مرة، على الرغم من أنه يفقد وحدة المعالجة المركزية (CPU) في القسم الحرج للقراءة
- يوجد بالفعل كاتب واحد فقط في كل مرة، على الرغم من أنه يفقد وحدة المعالجة المركزية (CPU) في القسم الحرج للكتابة
- لا يقرأ القارئ إلا عندما يكون هناك شيء للقراءة في الجدول
- لا يكتب الكاتب إلا عندما يتم قراءة المصفوفة بالكامل

