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. ایجاد نخهای اجرایی
ممکن است برنامههایی وجود داشته باشند که در آنها بخشهایی از کد بهطور «همزمان» در نخهای اجرایی مختلف اجرا شوند. وقتی میگوییم threadها بهطور همزمان اجرا میشوند، اغلب این یک نامگذاری نادرست است. اگر ماشین تنها یک پردازنده داشته باشد – که هنوز هم اغلب چنین است – threadها این پردازنده را به اشتراک میگذارند: هر یک به نوبت برای مدت کوتاهی (چند میلیثانیه) به آن دسترسی پیدا میکنند. این همان چیزی است که توهم اجرای موازی را ایجاد میکند. میزان زمانی که به یک thread اختصاص داده میشود به عوامل مختلفی بستگی دارد، از جمله اولویت آن، که یک مقدار پیشفرض دارد اما میتواند به صورت برنامهای نیز تنظیم شود. هنگامی که یک thread پردازنده را در اختیار دارد، معمولاً از آن برای تمام مدت زمان اختصاصیافته به خود استفاده میکند. با این حال، ممکن است آن را زودتر آزاد کند:
- با انتظار برای یک رویداد (انتظار، پیوست، تعلیق)
- با ورود به حالت خواب برای مدت زمان مشخصی (sleep)
- یک نخ T ابتدا توسط سازندهاش ایجاد میشود
ThreadStart از نوع delegate است و پروتوتایپ یک تابع بدون پارامتر را تعریف میکند:
یک ساخت معمولی به شرح زیر است:
تابع run که بهعنوان پارامتر ارسال میشود، هنگام راهاندازی نخ اجرا خواهد شد.
- اجرای نخ T توسط T.Start() آغاز میشود: تابع [run] که به کانستراکتور T پاس داده شده است، سپس توسط تار T اجرا خواهد شد. برنامهای که دستور T.start() را اجرا میکند، منتظر پایان کار تار T نمیماند: بلافاصله به دستور بعدی میرود. بنابراین ما دو وظیفه داریم که بهطور موازی در حال اجرا هستند. آنها اغلب باید بتوانند با یکدیگر ارتباط برقرار کنند تا پیشرفت کار مشترک را پیگیری کنند. این مسئله، مشکل همگامسازی رشتهها است.
- پس از راهاندازی، وظیفه thread بهصورت مستقل اجرا میشود. این وظیفه زمانی متوقف میشود که تابع 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
' پایان 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 اجرای پنج رشته را بهطور موازی آغاز کرد و اجرای خود را قبل از آنها به پایان رساند. عملیات
اجرای نخ tâches[i] را آغاز میکند، اما پس از انجام این کار، اجرای برنامه بلافاصله با دستور بعدی ادامه مییابد و منتظر پایان کار نخ نمیماند.
- تمام رشتههای ایجاد شده باید متد affiche را اجرا کنند. ترتیب اجرا غیرقابل پیشبینی است. اگرچه در این مثال، به نظر میرسد ترتیب اجرا از ترتیب درخواستهای اجرا پیروی میکند، اما نمیتوان از این موضوع نتیجهگیری کلی کرد. سیستمعامل در اینجا دارای ۶ نخ و یک پردازنده است. این سیستم پردازنده را بر اساس قوانین خود به این ۶ نخ تخصیص خواهد داد.
- نتایج، خروجی متد Sleep را نشان میدهند. در این مثال، تِرد 0 اولین تِردی است که متد affiche را اجرا میکند. پیام شروع اجرا نمایش داده میشود، و پس از آن متد Sleep اجرا میشود که آن را برای ۱ ثانیه معلق میکند. سپس پردازنده را از دست میدهد و در اختیار یک رشته دیگر قرار میگیرد. این مثال نشان میدهد که این رشته ۱ است که آن را به دست میآورد. رشته ۱ نیز مانند سایر رشتهها، همین توالی را دنبال میکند. هنگامی که دوره خواب یک ثانیهای رشته ۰ به پایان میرسد، اجرای آن میتواند از سر گرفته شود. سیستم پردازنده را به آن اختصاص میدهد و میتواند اجرای متد 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 (پروتکل انتقال فایل) را که فایلها را به مشتریان خود تحویل میدهد در نظر بگیریم، میدانیم که انتقال یک فایل گاهی اوقات میتواند چندین ساعت طول بکشد. البته، این که یک مشتری به مدت طولانی سرور را به انحصار خود درآورد، منتفی است. آنچه معمولاً انجام میشود این است که سرور به ازای تعداد مشتریان، به همان تعداد نخ اجرایی (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 استفاده میشود.
- شمارنده خوانده میشود
- رشته به مدت ۱ ثانیه متوقف میشود. بنابراین کنترل پردازنده را از دست میدهد
- شمارنده افزایش مییابد
مرحله ۲ تنها برای مجبور کردن نخ به از دست دادن پردازنده وجود دارد. سپس پردازنده به نخ دیگری واگذار میشود. در عمل، هیچ تضمینی وجود ندارد که یک نخ بین لحظهای که شمارنده را میخواند و لحظهای که آن را افزایش میدهد، قطع نشود. این خطر وجود دارد که پردازنده بین لحظهای که مقدار شمارنده خوانده میشود و لحظهای که مقدار آن با افزایش ۱ نوشته میشود، از دست برود. در واقع، عملیات افزایش شامل چندین دستور پایه در سطح پردازنده است که ممکن است قطع شوند. بنابراین، مرحله ۲ که شامل یک خواب یک ثانیهای است، تنها برای ساماندهی این خطر وجود دارد. نتایج حاصل به شرح زیر است:
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 استفاده خواهیم کرد:

در اینجا، ما فقط از سازندها و متدهای زیر استفاده خواهیم کرد:
یک شیء همگامسازی M ایجاد میکند | |
رشتهی T1 که عملیات M.WaitOne() را اجرا میکند، مالکیت شیء همگامسازی M را درخواست میکند. اگر مایتکس M توسط هیچ رشتهای در دست گرفته نشده باشد (که در ابتدا همینطور است)، به نخ T1 که آن را درخواست کرده است، «اعطا» میشود. اگر کمی بعد، نخی T2 همان عملیات را امتحان کند، مسدود خواهد شد. این به این دلیل است که یک موتکس در هر لحظه فقط میتواند متعلق به یک نخ باشد. هنگامی که نخ T1 موتکس M را که در اختیار دارد آزاد کند، آن آزاد خواهد شد. بنابراین ممکن است چندین نخ در حین انتظار برای موتکس M مسدود شوند. | |
تراشه T1 که عملیات M.ReleaseMutex() را انجام میدهد، مالکیت متاکس M.Lorsque را واگذار میکند؛ تراشه T1 پردازنده را از دست خواهد داد، و سیستم ممکن است آن را به یکی از رشتههایی که منتظر mutex M هستند اختصاص دهد. تنها یکی به نوبت آن را به دست میآورد؛ سایر رشتههای منتظر M مسدود باقی میمانند |
یک موتکس M دسترسی به یک منبع مشترک R را مدیریت میکند. یک نخ از طریق M.WaitOne() درخواست منبع R را میکند و از طریق 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. همگامسازی مبتنی بر رویداد
بیایید وضعیت زیر را در نظر بگیریم که گاهی به آن سناریوی تولیدکننده-مصرفکننده گفته میشود.
- یک آرایه داریم که برخی فرایندها (تولیدکنندگان) در آن دادهنویسی میکنند و برخی دیگر (مصرفکنندگان) آن را میخوانند.
- تولیدکنندگان از نظر یکدیگر برابرند اما بهطور متقابل انحصاری عمل میکنند: تنها یک تولیدکننده در هر لحظه میتواند دادهها را در آرایه قرار دهد.
- مصرفکنندگان همه برابر اما بهطور متقابل انحصاری هستند: تنها یک خواننده در هر لحظه میتواند دادههای ذخیرهشده در آرایه را بخواند.
- یک مصرفکننده تنها زمانی میتواند از آرایه دادهای بخواند که یک تولیدکننده دادهای را در آن نوشته باشد، و یک تولیدکننده تنها زمانی میتواند دادهی جدید را در آرایه بنویسد که دادهی موجود در آن مصرف شده باشد.
در این زمینه، میتوانیم بین دو منبع مشترک تمایز قائل شویم:
- آرایه قابل نوشتن
- آرایهٔ فقط-خواندنی
دسترسی به این دو منبع مشترک را میتوان با ماژولهای همگامسازی (mutexes) کنترل کرد، همانطور که قبلاً دیدیم، به طوری که برای هر منبع یک ماژول وجود دارد. هنگامی که یک مصرفکننده، جدول فقط-خواندنی را به دست آورد، باید بررسی کند که واقعاً دادهای در آن وجود دارد. از یک رویداد برای اطلاعرسانی این موضوع به آن استفاده خواهد شد. به همین ترتیب، یک تولیدکننده که جدول فقط-نوشتنی را به دست آورده است، باید منتظر بماند تا زمانی که یک مصرفکننده آن را خالی کند. در اینجا نیز از یک رویداد استفاده خواهد شد.
رویدادهای مورد استفاده بخشی از کلاس AutoResetEvent را تشکیل میدهند:

این نوع رویداد مشابه یک متغیر بولی است اما از انتظارهای فعال یا نیمهفعال اجتناب میکند. بنابراین، اگر مجوز نوشتن توسط یک متغیر بولی peutEcrire کنترل شود، یک تولیدکننده قبل از نوشتن، کدی از نوع زیر را اجرا خواهد کرد:
یا
در روش اول، نخ بهطور غیرضروری پردازنده را مشغول نگه میدارد. در روش دوم، هر ۱۰۰ میلیثانیه وضعیت متغیر بولی peutEcrire را بررسی میکند. کلاس AutoResetEvent امکان بهبود بیشتری را فراهم میکند: نخ زمانی که رویدادی که منتظر آن است رخ داده باشد، درخواست بیدار شدن خواهد داد:
AutoEvent peutEcrire=new AutoResetEvent(false) ' peutEcrire=false;
....
peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai
عملیات
متغیر بولهای peutEcrire را روی false تنظیم میکند. این عملیات
اجرا شده توسط یک نخ باعث میشود که آن نخ در صورتی که متغیر بولی 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 پردازنده را از دست میدهد
- یک خواننده تنها زمانی میخواند که چیزی برای خواندن در آرایه وجود داشته باشد
- یک نویسنده تنها پس از اینکه آرایه بهطور کامل خوانده شده باشد، مینویسد

