کلاسی به نام نخ
در داتنت، کلاسی به نام Thread قرار داده شده است که عملیات نخ را در آن کپسوله کرده است. این کلاس بهصورت Sealed (عقیم) است. یعنی قابلیت ارثبری از آن وجود ندارد. در این کلاس تعدادی متد و ویژگی برای مدیریت رفتار نخ وجود دارد که برخی از آنها را با هم بررسی میکنیم.
ایجاد یک نخ
در گام نخست، یک نمونه از کلاس Thread ایجاد کنید. یکی از سازندههای کلاس Thread بهاین صورت است.
public Thread(ThreadStart entryPoint);
توجه داشته باشید که entryPoint متدی است که Thread آن را اجرا میکند. آدرس آن را به متد ThreadStart میدهد. نکته جالب توجه این است که ThreadStart یک Delegate است (یعین آدرس متد را درون خود نگاه میدارد، برنامهنویسان C و C++ آن را بهنام Pointer To Function میشناسند.) حتما در ذهن داشهت باشید که متدی که آدرس آن به ThreadStart داده میشود باید حتما از نوع Void باشد، یعنی مقداری نگیرد و مقداری را برنگرداند. به این چند خط کد مثال توجه بکنید:
static void DisplayMessage()
{
Thread.Sleep(1000);
Console.WriteLine("Thread {0} execute this method",
Thread.CurrentThread.Name);
}
static void Main(string[] args)
{
Console.WriteLine("Main Thread Started Thread.CurrentThread.Name);
Thread thrd1 = new Thread(new ThreadStart(DisplayMessage));
thrd1.Name = "Thread Display Message";
thrd1.Start();
Console.WriteLine("Main Thread Exited.", Thread.CurrentThread.Name);
Console.ReadLine();
}
}
اما خروجی مثال بالا:
Main Thread Started
Main Thread Exited
Thread Display Message execute this method
از این مثال میتوان به این نتیجه رسید که thrd1 هیچ تداخلی با نخ اصلی ندارد و پس از اجرای نخ اصلی به کار خود خاتمه میدهد. مثال بالا را این بار با چند نخ بررسی میکنیم که بهخوبی عدم تداخل نخها جا بیافتد.
static void Main(string[] args)
{
Console.WriteLine("Main Thread Started.", Thread.CurrentThread.Name);
Thread thrd1 = new Thread(new ThreadStart(DisplayMessage));
thrd1.Name = "Thread Display Message 1";
Thread thrd2 = new Thread(new ThreadStart(DisplayMessage));
thrd2.Name = "Thread Display Message 2";
Thread thrd3 = new Thread(new ThreadStart(DisplayMessage));
thrd3.Name = "Thread Display Message 3";
thrd1.Start();
thrd2.Start();
thrd3.Start();
Console.WriteLine("Main Thread Exited.", Thread.CurrentThread.Name);
Console.ReadLine();
}
}
و اما خروجی
Main Thread Started
Main Thread Exited
Thread Thread Display Message 1 executed this method
Thread Thread Display Message 2 executed this method
Thread Thread Display Message 3 executed this method
همانطور که میبینید واقعا Threadها هیچ تداخلی با هم ندارند.
دادن متغیر به یک Thread:
در دو مثال بالا، ما نخی داشتیم که پارامتر ورودی نداشت، اما اگر بخواهیم به نخ خود یک پارامتر هم پاس بدهیم، چگونه این کار را انجام میدهیم؟
یکی دیگر از سازندههای کلاس Thread، شی ParameterizedThreadStart را بهعنوان ورودی قبول میکنند. این شی هم Delegate هست، اما میتواند آدرس متدی را در خود ذخیره کند که پارامتر ورودی قبول میکند.
static void DisplayMessage(object Message)
{
Thread.Sleep(1000);
Console.WriteLine("Thread {0} execute this
method with {1} as parameter",
Thread.CurrentThread.Name, Message);
Thread thrd1 = new Thread(new ParameterizedThreadStart(DisplayMessage));
thrd1.Name = "Thread Display Message 1";
شاید این مثال خیلی کاربردی بهنظر نرسد و اساسا هدف استفاده از نخها را زیر سوال ببرد. اما این متد فرضی را در نظر بگیرید:
static void SendMail(string from,
string to, string body, DateTime
sendDate, Guid mailId);
با ایجاد یک متد که نسبت به ارسال ایمیل اقدام میکند، میتوانیم عملیات ارسال ایمیل را که عملیاتی طولانی و نیازمند پیمودن مراحل مختلفی از جمله اتصال به سرور، اتصال به سرور مقصد، و ارسال پیام است را برای هر نخ به صورت جداگانه انجام دهیم و سرعت را بالا ببریم.
سوال بسیار مهم این است که از کجا بدانیم یک نخ که اجرا شده است، چه زمانی به انتهای کار خود میرسد؟ اگر ما متوجه نشویم که ارسال ایمیل به پایان رسیده است، طبیعتا نمیتوانیم به کاربر اطلاع بدهیم و استفاده از نخها بیفایده خواهد بود.
برای اینکار کلاس Thread یک ویژگی و متد را در اختیار برنامهنویس قرار میدهد.
while (thrd.IsAlive)
{
Console.Clear();
Console.WriteLine("Sending mail...");
}Console.WriteLine("Sending Completed");
وقتی نخ به انتهای عملیات خود میرسد، شرط غلط میشود و از حلقه خارج شده و پیغام میفرستد. استفاده از متد Join یکی دیگر از راههای تعیین تمام شدن نخ است. این متد نخ را به حالت بلوکه میبرد تا زمانی که کارش تمام شود.
راه دیگه برای تعیین تمام شدن Thread استفاده از متد Join است این متد Thread رو block می کند تا زمانی که کارش تمام شود این متد دو overload دارد.
پشت و جلو
داتنت دو نوع نخ دارد، نخ جلویی (Foreground) و دیگری پشتی (Background). باید توجه کرد که وقتی یک فرآیند به کار خود خاتمه میدهد، تمامی نخهای آن خاتمه پیدا میکنند. در این زمان، نخهای جلویی نسبت به نخهای پشتی اولویت بیشتری دارند. برای تعیین این ویژگی از هر نخ، کافی است IsBackground را به True تغییر دهیم.
اولویت نخ
همانطور که قطعا میدانید، اولویت بندی نخها و فرآیندها هر دو یکسان سات. برخی از نخها باید زودتر از نخهای دیگر اجرا شوند و در واقع اولویت بیشتری نسبت به دیگر نخها دارند. تعیین اولویت اجرای نخها با یک متغیر به نام ThreadPriority انجام میشود. این نوع داده شمارشی است و محتویات آن بهاین صورت است:
public enum ThreadPriority
{
Lowest,
BelowNormal,
Normal,
AboveNormal,
Highest
}
فراخوانی آسنکرون، راهیدیگر مشابه نخها
در داتنت امکانی به اسم فراخوانی آسنکرون وجود دارد. در این مدل ما چیزی شبیه به نخها را خواهیم داشت، ولی با نخها کاملا یکسان نیست. به این تکه کد نگاهی بیاندازید:
using System;
using System.Threading;
class Program
{
static void Display()
{
Console.WriteLine("Test...");
System.Threading.Thread.Sleep(1000);
}
static void Main(string[] args)
{
for (int i = 0; i < 30; i++)
{
Display();
}
}
}
در این خروجی، عبارت Test… چاپ میشود. بعد نرمافزار 1 ثانیه صبر میکند و دوباره عبارت Test… را چاپ میکند. این کار تا ثانیه سیام ادامه پیدا میکند. حالا چطور میشود این زمان را کاهش داد؟ میتوانیم قطعههای کوچکتر برنامه را به نخهای مختلف بدهیم، اما راهحل اگر نه بهتر، بلکه معادلی با این امر وجود دارد: آسنکرون. ما میتوانیم با تعریف یک Delegate این کار را انجام دهیم.
public delegate void DisplayCallback();
static DisplayCallback callback = new DisplayCallback(Display);
اگر این دستور را لابهلای کدهای بالا بگذارید، مشاهده میشود که خروجی بسیار سریعتر از حالت اول بهچاپ رسید. علت این امر چیست؟
زمانی که هر متد فراخوانی میشود، فراخواننده منتظر میماند تا فراخواننده کارش تمام شود و بعد کنترل را در دست بگیرد. اما در آسنکرون همچنین چیزی اتفاق نمیافتد و زمانی که متدی بخواهد بهصورت آسنکرون اجرا شود، برای متد مورد نظر یک نخ در نظر میگیرد و در ThreadPool قرار میدهد و از ThreadPool آنرا اجرا میکند. بهزبان سادهتر، وقتی ما متد Display را اجرا میکنیم، نخی به آن نسبت داده میشود و در ThreadPool قرار میگیرد و برای هر بار فراخوانی Display این عملیات اجرا میشوند. اما ThreadPool بهطور پیشفرض دارای محدود 25 نخ است. اما وقتی ما بخواهیم 30 نخ را داخل ThreadPool بگذاریم، چه اتفاقی میافتد؟
زمانی که به بیست و ششمین فراخوانی میرسیم، نرم افزار در آن لحظه به دنبال جای خالی میگردد تا در آنجا مقیم شود. اگر پیدا نکرد منتظر میماند تا هر نخی که کارش تمام شد، جای آن بنشیند.
امیربهاالدین سبطالشیخ