Skip to content

8. رشته‌های اجرایی

8.1. مقدمه

وقتی یک برنامه اجرا می‌شود، در یک جریان اجرایی به نام نخ اجرا می‌گردد. کلاس .NET که مدل‌سازی یک thread را بر عهده دارد، کلاس System.Threading.Thread است و تعریف زیر را دارد:

Image

ما تنها از برخی از ویژگی‌ها و متدهای این کلاس استفاده خواهیم کرد:

CurrentThread - propriété statique
رشته‌ای را که در حال اجراست بازمی‌گرداند
Name - propriété d'objet
نام نخ
isAlive - propriété d'objet
نشان می‌دهد که آیا نخ فعال است (true) یا خیر (false)
Start - méthode d'objet
یک تاپیک را آغاز می‌کند
Abort - méthode d'objet
پایان دادن دائمی به یک نخ
Sleep(n) - méthode statique
یک تار را برای n میلی‌ثانیه معلق می‌کند
Suspend() - méthode d'objet
اجرای یک نخ را به‌طور موقت معلق می‌کند
Resume() - méthode d'objet
اجرای یک نخ معلق‌شده را از سر می‌گیرد
Join() - méthode d'objet
عملیات مسدودکننده – منتظر پایان نخ می‌شود قبل از اینکه به دستور بعدی ادامه دهد

بیایید نگاهی به یک برنامه ساده بیندازیم که وجود یک نخ اجرای اصلی را نشان می‌دهد، نخی که در آن تابع 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. ایجاد نخ‌های اجرایی

ممکن است برنامه‌هایی وجود داشته باشند که در آن‌ها بخش‌هایی از کد به‌طور «هم‌زمان» در نخ‌های اجرایی مختلف اجرا شوند. وقتی می‌گوییم threadها به‌طور هم‌زمان اجرا می‌شوند، اغلب این یک نام‌گذاری نادرست است. اگر ماشین تنها یک پردازنده داشته باشد – که هنوز هم اغلب چنین است – threadها این پردازنده را به اشتراک می‌گذارند: هر یک به نوبت برای مدت کوتاهی (چند میلی‌ثانیه) به آن دسترسی پیدا می‌کنند. این همان چیزی است که توهم اجرای موازی را ایجاد می‌کند. میزان زمانی که به یک thread اختصاص داده می‌شود به عوامل مختلفی بستگی دارد، از جمله اولویت آن، که یک مقدار پیش‌فرض دارد اما می‌تواند به صورت برنامه‌ای نیز تنظیم شود. هنگامی که یک thread پردازنده را در اختیار دارد، معمولاً از آن برای تمام مدت زمان اختصاص‌یافته به خود استفاده می‌کند. با این حال، ممکن است آن را زودتر آزاد کند:

  • با انتظار برای یک رویداد (انتظار، پیوست، تعلیق)
  • با ورود به حالت خواب برای مدت زمان مشخصی (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. پس از راه‌اندازی، وظیفه thread به‌صورت مستقل اجرا می‌شود. این وظیفه زمانی متوقف می‌شود که تابع start که در حال اجرای آن است، کار خود را به پایان رساند.
  1. می‌توان سیگنال‌های خاصی را به وظیفه T ارسال کرد:
    1. تابع T.Suspend() به آن می‌گوید که موقتاً مکث کند
    2. تابع T.Resume() به آن می‌گوید کارش را از سر گیرد
    3. T.Abort() به آن می‌گوید که به‌طور دائم متوقف شود
  1. ما همچنین می‌توانیم با استفاده از 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
        ' پایان main
        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")))
        ' خواب برای ۱ ثانیه
        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 را اجرا می‌کند، پنج رشته دیگر ایجاد می‌کند که وظیفه اجرای متد استاتیک 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 اجرای پنج رشته را به‌طور موازی آغاز کرد و اجرای خود را قبل از آن‌ها به پایان رساند. عملیات
             ' راه‌اندازی نخ i
            tâches(i).Start()

اجرای نخ tâches[i] را آغاز می‌کند، اما پس از انجام این کار، اجرای برنامه بلافاصله با دستور بعدی ادامه می‌یابد و منتظر پایان کار نخ نمی‌ماند.

  • تمام رشته‌های ایجاد شده باید متد affiche را اجرا کنند. ترتیب اجرا غیرقابل پیش‌بینی است. اگرچه در این مثال، به نظر می‌رسد ترتیب اجرا از ترتیب درخواست‌های اجرا پیروی می‌کند، اما نمی‌توان از این موضوع نتیجه‌گیری کلی کرد. سیستم‌عامل در اینجا دارای ۶ نخ و یک پردازنده است. این سیستم پردازنده را بر اساس قوانین خود به این ۶ نخ تخصیص خواهد داد.
  • نتایج، خروجی متد Sleep را نشان می‌دهند. در این مثال، تِرد 0 اولین تِردی است که متد affiche را اجرا می‌کند. پیام شروع اجرا نمایش داده می‌شود، و پس از آن متد Sleep اجرا می‌شود که آن را برای ۱ ثانیه معلق می‌کند. سپس پردازنده را از دست می‌دهد و در اختیار یک رشته دیگر قرار می‌گیرد. این مثال نشان می‌دهد که این رشته ۱ است که آن را به دست می‌آورد. رشته ۱ نیز مانند سایر رشته‌ها، همین توالی را دنبال می‌کند. هنگامی که دوره خواب یک ثانیه‌ای رشته ۰ به پایان می‌رسد، اجرای آن می‌تواند از سر گرفته شود. سیستم پردازنده را به آن اختصاص می‌دهد و می‌تواند اجرای متد affiche را به پایان برساند.

بیایید برنامه خود را برای خاتمه دادن به متد Main با دستورالعمل‌های زیر اصلاح کنیم:

         ' پایان برنامهٔ اصلی
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

اجرای برنامه جدید خروجی زیر را تولید می‌کند:

fin du thread main

رشته‌هایی که توسط تابع Main ایجاد شده‌اند، اجرا نمی‌شوند. این دستور است

        Environment.Exit(0)

که باعث این امر می‌شود: این تابع تمام نخ‌های برنامه را متوقف می‌کند، نه فقط نخ 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 (پروتکل انتقال فایل) را که فایل‌ها را به مشتریان خود تحویل می‌دهد در نظر بگیریم، می‌دانیم که انتقال یک فایل گاهی اوقات می‌تواند چندین ساعت طول بکشد. البته، این که یک مشتری به مدت طولانی سرور را به انحصار خود درآورد، منتفی است. آنچه معمولاً انجام می‌شود این است که سرور به ازای تعداد مشتریان، به همان تعداد نخ اجرایی (thread) ایجاد کند. سپس هر نخ مسئول رسیدگی به یک مشتری خاص است. از آنجایی که پردازنده به صورت چرخه‌ای بین تمام نخ‌های فعال روی دستگاه به اشتراک گذاشته می‌شود، سرور زمان کمی را با هر مشتری صرف می‌کند و بدین ترتیب تضمین می‌کند که خدمات به طور همزمان ارائه می‌شوند.

8.4. دسترسی به منابع مشترک

در مثال کلاینت-سرور فوق، هر تار (thread) عمدتاً به طور مستقل به یک کلاینت خدمت می‌کند. با این حال، تارها ممکن است برای ارائه خدمتی که کلاینتشان درخواست کرده است، به ویژه هنگام دسترسی به منابع مشترک، نیاز به همکاری داشته باشند. نمودار بالا یادآور باجه‌های یک اداره بزرگ دولتی، مانند اداره پست، است که در آن در هر باجه یک کارمند به مشتری خدمت‌رسانی می‌کند. فرض کنید که این کارمندان گهگاه نیاز دارند از اسناد ارسالی مشتریانشان کپی بگیرند و تنها یک دستگاه کپی وجود دارد. دو کارمند نمی‌توانند همزمان از دستگاه فتوکپی استفاده کنند. اگر کارمند 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 استفاده می‌شود.

  1. شمارنده خوانده می‌شود
  2. رشته به مدت ۱ ثانیه متوقف می‌شود. بنابراین کنترل پردازنده را از دست می‌دهد
  3. شمارنده افزایش می‌یابد

مرحله ۲ تنها برای مجبور کردن نخ به از دست دادن پردازنده وجود دارد. سپس پردازنده به نخ دیگری واگذار می‌شود. در عمل، هیچ تضمینی وجود ندارد که یک نخ بین لحظه‌ای که شمارنده را می‌خواند و لحظه‌ای که آن را افزایش می‌دهد، قطع نشود. این خطر وجود دارد که پردازنده بین لحظه‌ای که مقدار شمارنده خوانده می‌شود و لحظه‌ای که مقدار آن با افزایش ۱ نوشته می‌شود، از دست برود. در واقع، عملیات افزایش شامل چندین دستور پایه در سطح پردازنده است که ممکن است قطع شوند. بنابراین، مرحله ۲ که شامل یک خواب یک ثانیه‌ای است، تنها برای سامان‌دهی این خطر وجود دارد. نتایج حاصل به شرح زیر است:

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 است، زیرا نخ قبلی هنوز آن را افزایش نداده است. آن هم به مدت 1 ثانیه مکث می‌کند.
  • در ۱ ثانیه، هر ۵ نخ فرصت اجرا پیدا می‌کنند و همگی مقدار ۰ را می‌خوانند.
  • وقتی یکی پس از دیگری بیدار می‌شوند، مقداری را که خوانده‌اند (مقدار ۰) افزایش داده و عدد ۱ را در شمارنده ثبت می‌کنند، همانطور که در برنامه اصلی (Main) تأیید شده است.

مشکل از کجا ناشی می‌شود؟ نخ دوم یک مقدار نادرست خواند زیرا نخ اول قبل از تکمیل وظیفه‌اش، یعنی به‌روزرسانی شمارنده در پنجره، متوقف شده بود. این ما را به مفهوم منبع بحرانی و بخش بحرانی در یک برنامه می‌رساند:

  • منبع بحرانی منبعی است که در هر لحظه تنها یک نخ می‌تواند آن را در اختیار داشته باشد. در اینجا، منبع بحرانی، شمارنده است.
  • بخش بحرانی یک برنامه، دنباله‌ای از دستورالعمل‌ها در جریان اجرای یک نخ است که در طی آن به یک منبع بحرانی دسترسی پیدا می‌کند. ما باید اطمینان حاصل کنیم که در طول این بخش بحرانی، تنها همان نخ به منبع دسترسی داشته باشد.

8.5. دسترسی انحصاری به یک منبع مشترک

در مثال ما، بخش بحرانی کدی است که بین خواندن شمارنده و نوشتن مقدار جدید آن قرار دارد:


         ' قرائت کنتور
        Dim valeur As Integer = cptrThreads
        'در انتظار
        Thread.Sleep(1000)
        ' افزایش شمارنده
        cptrThreads = valeur + 1

برای اجرای این کد، باید تضمین شود که یک نخ تنها نخ در حال اجرا باشد. ممکن است این نخ متوقف شود، اما در حالی که متوقف است، هیچ نخ دیگری نباید بتواند همان کد را اجرا کند. پلتفرم .NET چندین ابزار برای تضمین دسترسی تک‌نخی به بخش‌های حیاتی کد ارائه می‌دهد. ما از کلاس Mutex استفاده خواهیم کرد:

Image

در اینجا، ما فقط از سازندها و متدهای زیر استفاده خواهیم کرد:

public Mutex()
یک شیء همگام‌سازی M ایجاد می‌کند
public bool WaitOne()
رشته‌ی T1 که عملیات M.WaitOne() را اجرا می‌کند، مالکیت شیء همگام‌سازی M را درخواست می‌کند. اگر مایتکس M توسط هیچ رشته‌ای در دست گرفته نشده باشد (که در ابتدا همین‌طور است)، به نخ T1 که آن را درخواست کرده است، «اعطا» می‌شود. اگر کمی بعد، نخی T2 همان عملیات را امتحان کند، مسدود خواهد شد. این به این دلیل است که یک موتکس در هر لحظه فقط می‌تواند متعلق به یک نخ باشد. هنگامی که نخ T1 موتکس M را که در اختیار دارد آزاد کند، آن آزاد خواهد شد. بنابراین ممکن است چندین نخ در حین انتظار برای موتکس M مسدود شوند.
public void
ReleaseMutex()
تراشه T1 که عملیات M.ReleaseMutex() را انجام می‌دهد، مالکیت متاکس M.Lorsque را واگذار می‌کند؛ تراشه T1 پردازنده را از دست خواهد داد، و سیستم ممکن است آن را به یکی از رشته‌هایی که منتظر mutex M هستند اختصاص دهد. تنها یکی به نوبت آن را به دست می‌آورد؛ سایر رشته‌های منتظر M مسدود باقی می‌مانند

یک موتکس M دسترسی به یک منبع مشترک R را مدیریت می‌کند. یک نخ از طریق M.WaitOne() درخواست منبع R را می‌کند و از طریق M.ReleaseMutex() آن را آزاد می‌کند. یک بخش بحرانی کد که باید همزمان فقط توسط یک نخ اجرا شود، یک منبع مشترک است. اجرای بخش بحرانی را می‌توان به شرح زیر همگام‌سازی کرد:

M.WaitOne()
' فقط این نخ وارد اینجا می‌شود
'بخش بحرانی
....
M.ReleaseMutex()

که در آن M یک شیء Mutex است. البته، ضروری است که هرگز فراموش نکنید Mutex را که دیگر مورد نیاز نیست آزاد کنید، تا نخ دیگر بتواند وارد بخش بحرانی شود؛ در غیر این صورت، نخ‌هایی که منتظر mutex هستند که هرگز آزاد نمی‌شود، هرگز به پردازنده دسترسی پیدا نخواهند کرد. علاوه بر این، باید از بن‌بست (deadlock)، وضعیتی که در آن دو نخ منتظر یکدیگر هستند، جلوگیری کنید. توالی رویدادهای زیر را در نظر بگیرید:

  • یک نخ T1 یک متکس M1 را برای دسترسی به یک منبع مشترک R1 به دست می‌آورد
  • یک نخ T2 برای دسترسی به یک منبع مشترک R2، یک متکس M2 را به دست می‌آورد.
  • رشته T1 مِیتکس M2 را درخواست می‌کند. مسدود شده است.
  • رشته T2 در حال درخواست متقابل قفل (mutex) 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. همگام‌سازی مبتنی بر رویداد

بیایید وضعیت زیر را در نظر بگیریم که گاهی به آن سناریوی تولیدکننده-مصرف‌کننده گفته می‌شود.

  1. یک آرایه داریم که برخی فرایندها (تولیدکنندگان) در آن داده‌نویسی می‌کنند و برخی دیگر (مصرف‌کنندگان) آن را می‌خوانند.
  2. تولیدکنندگان از نظر یکدیگر برابرند اما به‌طور متقابل انحصاری عمل می‌کنند: تنها یک تولیدکننده در هر لحظه می‌تواند داده‌ها را در آرایه قرار دهد.
  3. مصرف‌کنندگان همه برابر اما به‌طور متقابل انحصاری هستند: تنها یک خواننده در هر لحظه می‌تواند داده‌های ذخیره‌شده در آرایه را بخواند.
  4. یک مصرف‌کننده تنها زمانی می‌تواند از آرایه داده‌ای بخواند که یک تولیدکننده داده‌ای را در آن نوشته باشد، و یک تولیدکننده تنها زمانی می‌تواند داده‌ی جدید را در آرایه بنویسد که داده‌ی موجود در آن مصرف شده باشد.

در این زمینه، می‌توانیم بین دو منبع مشترک تمایز قائل شویم:

    1. آرایه قابل نوشتن
    2. آرایهٔ فقط-خواندنی

دسترسی به این دو منبع مشترک را می‌توان با ماژول‌های همگام‌سازی (mutexes) کنترل کرد، همانطور که قبلاً دیدیم، به طوری که برای هر منبع یک ماژول وجود دارد. هنگامی که یک مصرف‌کننده، جدول فقط-خواندنی را به دست آورد، باید بررسی کند که واقعاً داده‌ای در آن وجود دارد. از یک رویداد برای اطلاع‌رسانی این موضوع به آن استفاده خواهد شد. به همین ترتیب، یک تولیدکننده که جدول فقط-نوشتنی را به دست آورده است، باید منتظر بماند تا زمانی که یک مصرف‌کننده آن را خالی کند. در اینجا نیز از یک رویداد استفاده خواهد شد.

رویدادهای مورد استفاده بخشی از کلاس AutoResetEvent را تشکیل می‌دهند:

Image

این نوع رویداد مشابه یک متغیر بولی است اما از انتظارهای فعال یا نیمه‌فعال اجتناب می‌کند. بنابراین، اگر مجوز نوشتن توسط یک متغیر بولی peutEcrire کنترل شود، یک تولیدکننده قبل از نوشتن، کدی از نوع زیر را اجرا خواهد کرد:

while(peutEcrire==false)        ' attente active

یا

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

در روش اول، نخ به‌طور غیرضروری پردازنده را مشغول نگه می‌دارد. در روش دوم، هر ۱۰۰ میلی‌ثانیه وضعیت متغیر بولی peutEcrire را بررسی می‌کند. کلاس AutoResetEvent امکان بهبود بیشتری را فراهم می‌کند: نخ زمانی که رویدادی که منتظر آن است رخ داده باشد، درخواست بیدار شدن خواهد داد:

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;
....
peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

عملیات

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

متغیر بوله‌ای peutEcrire را روی false تنظیم می‌کند. این عملیات

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

اجرا شده توسط یک نخ باعث می‌شود که آن نخ در صورتی که متغیر بولی peutEcrire درست باشد، ادامه یابد؛ در غیر این صورت، تا زمانی که درست شود مسدود می‌ماند. یک نخ دیگر آن را از طریق عملیات peutEcrire.Set() به مقدار true یا از طریق عملیات peutEcrire.Reset() به مقدار false تنظیم خواهد کرد.

برنامه تولیدکننده-مصرف‌کننده به شرح زیر است:


' استفاده از نخ‌های خواندن و نوشتن
'نشان‌دهنده استفاده همزمان از منابع مشترک و همگام‌سازی است

' گزینه‌ها
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
            'منتظر بمانید ۱ ثانیه
            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
            'در حال انتظار ۱ ثانیه
            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 پردازنده را از دست می‌دهد
  • یک خواننده تنها زمانی می‌خواند که چیزی برای خواندن در آرایه وجود داشته باشد
  • یک نویسنده تنها پس از اینکه آرایه به‌طور کامل خوانده شده باشد، می‌نویسد