Skip to content

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

8.1. مقدمة

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

Image

سنستخدم فقط بعض خصائص وأساليب هذه الفئة:

CurrentThread - خاصية ثابتة
تُرجع مؤشر الخيط الذي يعمل حاليًا
الاسم - خاصية الكائن
اسم الخيط
isAlive - خاصية الكائن
يشير إلى ما إذا كان الخيط نشطًا (صحيح) أم لا (خطأ)
Start - طريقة الكائن
يبدأ تنفيذ مؤشر الترابط
إلغاء - طريقة الكائن
توقف تنفيذ مؤشر الترابط بشكل دائم
Sleep(n) - طريقة ثابتة
يوقف مؤقتًا مؤشر ترابط لمدة n مللي ثانية
Suspend() - طريقة كائن
تعلق مؤقتًا تنفيذ مؤشر ترابط
Resume() - طريقة الكائن
يستأنف تنفيذ مؤشر ترابط تم تعليقه
Join() - طريقة الكائن
عملية حجب - تنتظر انتهاء الخيط قبل الانتقال إلى التعليمات التالية

لنلقِ نظرة على تطبيق بسيط يوضح وجود مؤشر ترابط التنفيذ الرئيسي، وهو المؤشر الذي تعمل فيه دالة 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)
  1. يتم إنشاء الخيط T أولاً بواسطة منشئه
Public Sub New(ByVal start As ThreadStart)

ThreadStart هو من نوع delegate ويحدد النموذج الأولي لدالة بدون معلمات:

Public Delegate Sub ThreadStart()

فيما يلي بنية نموذجية:

dim T as Thread=new Thread(new ThreadStart(run));

سيتم تنفيذ الدالة run التي تم تمريرها كمعلمة عند تشغيل الخيط.

  1. يتم بدء تنفيذ الخيط T بواسطة T.Start(): ثم يتم تنفيذ الدالة [run] التي تم تمريرها إلى منشئ T بواسطة الخيط T. لا ينتظر البرنامج الذي ينفذ عبارة T.Start() انتهاء المهمة T: بل ينتقل فورًا إلى العبارة التالية. لدينا الآن مهمتان تعملان بالتوازي. وغالبًا ما تحتاجان إلى التواصل مع بعضهما البعض لمعرفة حالة العمل المشترك الذي يتعين القيام به. هذه هي مشكلة تزامن الخيوط.
  1. بمجرد تشغيله، يعمل الخيط بشكل مستقل. سيتوقف عندما تنتهي وظيفة البدء التي ينفذها من عملها.
  1. يمكننا إرسال إشارات معينة إلى المهمة T:
    1. T.Suspend() يطلب منه التوقف مؤقتًا
    2. T.Resume() تأمره باستئناف عمله
    3. T.Abort() تأمره بالتوقف نهائيًا
  1. يمكنك أيضًا انتظار انتهاء تنفيذها باستخدام 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 خيوط بالتوازي وانتهت من التنفيذ قبلها.
            ' on lance l'exécution du thread i
            tâches(i).Start()

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

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

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

        ' fin de main
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

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

fin du thread main

لا يتم تنفيذ الخيوط التي أنشأتها الدالة Main. إنها العبارة

        Environment.Exit(0)

هي التي تقوم بذلك: فهي تنهي جميع الخيوط في التطبيق، وليس فقط الخيط الرئيسي. الحل لهذه المشكلة هو أن تنتظر الطريقة 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.

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

الخطوة 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:

Image

هنا، سنستخدم فقط المنشئات والأساليب التالية:

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

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

M.WaitOne()
' le thread est seul à entrer ici
' section critique
....
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. التزامن القائم على الأحداث

لننظر إلى الموقف التالي، الذي يُشار إليه أحيانًا باسم سيناريو المنتج والمستهلك.

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

في هذا الشرح، يمكننا التمييز بين موردين مشتركين:

    1. الجدول القابل للكتابة
    2. المصفوفة للقراءة فقط

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

ستكون الأحداث المستخدمة من فئة AutoResetEvent:

Image

هذا النوع من الأحداث مشابه للقيمة المنطقية (boolean) ولكنه يتجنب عمليات الانتظار النشطة أو شبه النشطة. وبالتالي، إذا تم التحكم في الوصول للكتابة بواسطة القيمة المنطقية *canWrite*، فسيقوم المنتج بتنفيذ كود مثل التالي قبل الكتابة:

while(peutEcrire==false)        ' attente active

أو

while(peutEcrire==false) ' attente semi-active
    Thread.Sleep(100)                ' attente de 100ms
end while

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

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;
....
peutEcrire.WaitOne() ' thread waits for peutEcrire event to change to true

العملية

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;

تقوم بتهيئة المتغير المنطقي canWrite إلى false. العملية

peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

التي ينفذها مؤشر ترابط ما تؤدي إلى استمرار ذلك المؤشر في العمل إذا كانت القيمة المنطقية 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) في القسم الحرج للكتابة
  • لا يقرأ القارئ إلا عندما يكون هناك شيء للقراءة في الجدول
  • لا يكتب الكاتب إلا عندما يتم قراءة المصفوفة بالكامل