مقدمه

در درس قبل با مفاهیم نظری همزمانی و نخ‌ها (Threads) آشنا شدیم. اکنون زمان آن است که به صورت عملی، اولین نخ ثانویه‌ی خود را ایجاد کنیم. هرچند در برنامه‌نویسی مدرن C#، ابزارهای سطح بالاتری مانند Task Parallel Library (TPL) و async/await وجود دارند که کار با همزمانی را بسیار ساده‌تر می‌کنند، اما درک نحوه‌ی کارکرد واحد سازنده‌ی اصلی یعنی کلاس System.Threading.Thread، برای فهم عمیق‌تر این مفاهیم ضروری است. در این درس، یاد می‌گیریم که چگونه به صورت دستی یک نخ جدید ایجاد کرده، آن را مدیریت کنیم و یک وظیفه را در پس‌زمینه اجرا نماییم تا نخ اصلی برنامه آزاد و پاسخگو باقی بماند.

کلاس System.Threading.Thread

این کلاس، نمایانگر یک نخ اجرایی در .NET است. با ساختن یک نمونه از این کلاس، ما یک نخ جدید در پروسه‌ی خود ایجاد می‌کنیم. برای اینکه به این نخ بگوییم چه کاری باید انجام دهد، آدرس متدی را که می‌خواهیم اجرا شود، به سازنده‌ی آن پاس می‌دهیم. این کار از طریق یک نماینده (delegate) انجام می‌شود.

ایجاد و شروع یک نخ جدید

فرآیند ایجاد و اجرای یک نخ جدید شامل سه مرحله است: تعریف کاری که باید انجام شود (یک متد)، ساختن یک نمونه از کلاس Thread با ارجاع به آن متد، و در نهایت فراخوانی متد Start() برای شروع اجرای نخ.

Copy Icon Program.cs
using System.Threading;

// 1. Define the work to be done on the secondary thread.
void DoWork()
{
    Console.WriteLine("Secondary thread started.");
    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine("Work in progress...");
        Thread.Sleep(500); // Pause for 500 milliseconds
    }
    Console.WriteLine("Secondary thread finished.");
}

// --- In Main method ---

Console.WriteLine("Main thread started.");

// 2. Create a new Thread object.
Thread backgroundThread = new Thread(DoWork);

// 3. Start the thread. Execution begins in DoWork().
backgroundThread.Start();

Console.WriteLine("Main thread continues to run independently.");

// Main thread does its own work...
for (int i = 0; i < 3; i++)
{
    Console.WriteLine("Main thread is working...");
    Thread.Sleep(300);
}

اگر این کد را اجرا کنید، خواهید دید که خروجی پیام‌های نخ اصلی و نخ ثانویه به صورت درهم آمیخته چاپ می‌شوند. این به وضوح نشان می‌دهد که دو نخ به صورت همزمان در حال اجرا هستند و نخ اصلی برای اتمام کار نخ ثانویه منتظر نمی‌ماند.

نخ‌های پیش‌زمینه در مقابل پس‌زمینه

نخ‌ها در .NET به دو نوع تقسیم می‌شوند:

  • نخ پیش‌زمینه (Foreground Thread): این حالت پیش‌فرض است. یک نخ پیش‌زمینه، پروسه‌ی برنامه را زنده نگه می‌دارد، حتی اگر نخ اصلی برنامه کار خود را تمام کرده باشد. برنامه تنها زمانی به طور کامل بسته می‌شود که تمام نخ‌های پیش‌زمینه‌ی آن به پایان برسند.
  • نخ پس‌زمینه (Background Thread): این نوع نخ، برنامه را زنده نگه نمی‌دارد. اگر تمام نخ‌های پیش‌زمینه‌ی یک برنامه به پایان برسند، هر نخ پس‌زمینه‌ای که هنوز در حال اجرا باشد، به صورت ناگهانی متوقف (terminate) می‌شود. این نوع نخ‌ها برای کارهای جانبی مانند لاگ‌گیری دوره‌ای یا نظارت بر وضعیت که می‌توانند در هر لحظه متوقف شوند، مناسب هستند.

برای تبدیل یک نخ به نخ پس‌زمینه، کافی است پراپرتی IsBackground آن را true قرار دهید.

Copy Icon Program.cs
Thread backgroundThread = new Thread(DoWork);
// Set the thread to run in the background.
backgroundThread.IsBackground = true;
backgroundThread.Start();

منتظر ماندن برای اتمام یک نخ

گاهی اوقات نخ اصلی نیاز دارد تا برای ادامه‌ی کار خود، منتظر بماند تا یک نخ ثانویه کارش را تمام کند. برای مثال، ممکن است یک نخ در حال محاسبه‌ی یک مقدار باشد و نخ اصلی برای استفاده از آن مقدار، به نتیجه‌ی آن نیاز داشته باشد. برای این کار از متد Join() استفاده می‌کنیم.

Copy Icon Program.cs
Console.WriteLine("Main thread: Starting background thread.");
Thread backgroundThread = new Thread(DoWork);
backgroundThread.Start();

Console.WriteLine("Main thread: Waiting for background thread to complete.");
// The main thread will block here until DoWork() is finished.
backgroundThread.Join();

Console.WriteLine("Main thread: Background thread has finished. The program can now exit.");

فراخوانی Join، نخ فعلی (در اینجا نخ اصلی) را مسدود می‌کند تا زمانی که نخی که متد روی آن فراخوانی شده (backgroundThread) کار خود را به اتمام برساند. این یک روش ساده برای همگام‌سازی بین نخ‌هاست.

ارسال داده به یک نخ

چگونه می‌توانیم هنگام شروع یک نخ، به آن داده ارسال کنیم؟ روش مدرن و ترجیح داده شده برای این کار، استفاده از یک عبارت لامبدا است. با استفاده از لامبدا، می‌توانیم متغیرهایی را از محدوده‌ی فعلی "capture" کرده و به متدی که قرار است روی نخ جدید اجرا شود، پاس دهیم.

void PrintNumber(int number)
{
    Console.WriteLine($"The number is: {number}");
}

int numberToPrint = 42;

// Use a lambda expression to call the method with a parameter.
Thread t = new Thread(() => PrintNumber(numberToPrint));
t.Start();

این روش بسیار تمیزتر و امن‌تر از روش‌های قدیمی‌تر مانند استفاده از نماینده‌ی ParameterizedThreadStart است، زیرا امنیت نوع را به طور کامل حفظ می‌کند.