چند نخی در سی شارپ — Multithreading

تصویر شاخص چند نخی در سی شارپ

#C یک زبان برنامه‌نویسی چندسبکی است که از سبک‌های مختلف برنامه‌نویسی مانند رویه‌ای، شی‌گرا و تابعی پشتیبانی می‌کند. یکی از ویژگی‌های اساسی سی شارپ پشتیبانی آن از چند نخی «Multithreading» است که به توسعه‌دهندگان این امکان را می‌دهد تا برنامه‌هایی بنویسند که بتوانند چندین وظیفه را به‌طور همزمان انجام دهند.

در این مقاله با مفاهیم پایه‌ای چند نخی در سی شارپ «#Multithreading in C»، مزایا، چالش‌ها، و روش‌های مختلف پیاده‌سازی آن مانند استفاده از کلاس‌های Thread، Task، کلمات کلیدی async و await، و کلاس ThreadPool آشنا خواهید شد.

مقدمه

در دنیای امروز که سرعت و عملکرد نرم‌افزارها نقش مهمی در تجربه کاربر ایفا می‌کند، بهره‌گیری از تکنیک‌های پیشرفته برنامه‌نویسی برای بهبود کارایی بیش از پیش اهمیت یافته است. یکی از این تکنیک‌ها، چند نخی است که به برنامه‌ها این امکان را می‌دهد تا چندین وظیفه را به‌طور همزمان اجرا کنند. زبان برنامه‌نویسی #C با فراهم‌سازی ابزارها و کلاس‌های متنوع برای پیاده‌سازی چند نخی، بستری قدرتمند جهت توسعه‌ی برنامه‌های سریع و پاسخ‌گو فراهم کرده است. اگر به دنبال آن هستید که عملکرد برنامه‌های خود را به سطح بالاتری ارتقا دهید و از تمام توان پردازنده بهره ببرید، این مقاله نقطه‌ی شروع مناسبی برای شما خواهد بود.

چند نخی در سی شارپ

چند نخی در سی شارپ #C یک تکنیک برنامه‌نویسی است که اجازه می‌دهد چندین نخ «Thread» به‌طور همزمان در یک فرآیند واحد اجرا شوند. هر نخ نشان‌دهنده‌ی یک مسیر اجرایی جداگانه است که امکان انجام موازی وظایف را فراهم می‌کند و باعث بهبود کارایی و عملکرد برنامه‌ها می‌شود.

مثال: پیاده‌سازی چند نخی با استفاده از کلاس Thread.

// Creating a thread using the Thread class in C#
using System;
using System.Threading;

class PStore
{
 static void Main()
 {
  // create a new thread
  Thread t = new Thread(Worker);

  // start the thread
  t.Start();

  // do some other work in the main thread
  for (int i = 1; i < 5; i++)
  {
   Console.WriteLine("Main thread doing some work");
   Thread.Sleep(100);
  }

  // wait for the worker thread to complete
  t.Join();

  Console.WriteLine("Done");
 }

 static void Worker()
 {
  for (int i = 1; i < 3; i++)
  {
   Console.WriteLine("Worker thread doing some work");
   Thread.Sleep(100);
  }
 }
}

خروجی

Main thread doing some work
Worker thread doing some work
Worker thread doing some work
Main thread doing some work
Main thread doing some work
Main thread doing some work
Done

توضیح: در مثال بالا، یک نخ جدید ایجاد و اجرا می‌شود که متد Worker را اجرا می‌کند، در حالی که نخ اصلی وظیفه خودش را انجام می‌دهد. هر دو نخ پیام‌هایی را چاپ می‌کنند و برای مدت کوتاهی متوقف می‌شوند (sleep). نخ اصلی منتظر می‌ماند تا نخ Worker کارش را تمام کند، سپس عبارت Done را چاپ می‌کند.

مثال دنیای واقعی

چندوظیفگی «Multitasking» به معنای اجرای هم‌زمان چندین وظیفه یا فرآیند در یک بازه زمانی مشخص است. سیستم‌عامل ویندوز نمونه‌ای از چندوظیفگی است، زیرا می‌تواند بیش از یک فرآیند را به‌طور همزمان اجرا کند؛ مانند اجرای هم‌زمان Google Chrome، Notepad، VLC Player و غیره. سیستم‌عامل برای اجرای این برنامه‌ها به‌صورت هم‌زمان از مفهومی به نام فرآیند «Process» استفاده می‌کند.

فرآیند «Process»: بخشی از سیستم‌عامل است که مسئول اجرای یک برنامه کاربردی می‌باشد.

نخ «Thread»: یک نخ فرآیندی سبک‌وزن است، یا به‌عبارت دیگر، نخ واحدی است که کد برنامه را اجرا می‌کند.

هر برنامه‌ای دارای منطق خاص خود است و نخ مسئول اجرای این منطق می‌باشد. هر برنامه به‌صورت پیش‌فرض دارای یک نخ است که وظیفه اجرای منطق برنامه را بر عهده دارد، و به این نخ “نخ اصلی” (Main Thread) گفته می‌شود. بنابراین، هر برنامه یا نرم‌افزاری به‌طور پیش‌فرض از مدل تک‌نخی «single-threaded» استفاده می‌کند.

مدل تک‌نخی

در مدل تک‌نخی «Single Threaded Model»، تمام فرآیندهای موجود در برنامه به‌صورت هماهنگ و پشت سر هم اجرا می‌شوند؛ به این معنا که هر فرآیند باید منتظر بماند تا فرآیند قبلی اجرای خود را کامل کند. این موضوع باعث می‌شود زمان پردازش افزایش یابد.

برای مثال، کلاسی به نام Geek داریم که شامل دو متد مختلف به نام‌های method1 و method2 است. در این حالت، نخ اصلی مسئول اجرای تمام این متدها است، بنابراین نخ اصلی این متدها را به ترتیب و یکی پس از دیگری اجرا می‌کند.

مدل تک نخی در سی شارپ

مثال: نمایش مدل تک‌نخی

// Single threaded model example
using System;
using System.Threading;

public class PStore
{
    public static void method1()
    {
        // It prints numbers from 0 to 4
        for (int i = 1; i < 5; i++)
        {

            Console.WriteLine("Method1 is : {0}", i);

            if (i == 2)
            {
                Thread.Sleep(100);
            }
        }
    }
    public static void method2()
    {
        // It prints numbers from 0 to 4
        for (int j = 1; j < 5; j++)
        {

            Console.WriteLine("Method2 is : {0}", j);
        }
    }
}

public class Program
{
    static public void Main()
    {

        // Calling static methods
        PStore.method1();
        PStore.method2();
    }
}

خروجی

Method1 is : 1
Method1 is : 2
Method1 is : 3
Method1 is : 4
Method2 is : 1
Method2 is : 2
Method2 is : 3
Method2 is : 4

توضیح: در مثال بالا، متد Method2 منتظر می‌ماند تا متد Method1 به‌طور کامل اجرا شود و تا زمانی که اجرای آن تمام نشود، Method2 اجرا نخواهد شد. این یکی از معایب مدل تک‌نخی است. برای غلبه بر این مشکل، از مدل چند نخی استفاده می‌شود، که در آن می‌توان برنامه‌ها و نخ‌های مختلف را به‌صورت همزمان اجرا کرد.

چند نخی همچنین باعث استفاده بهینه‌تر از CPU می‌شود، زیرا چند نخی بر اساس مفهوم اشتراک‌گذاری زمانی «time-sharing» کار می‌کند؛ به این معنا که هر نخ زمان مشخصی برای اجرا دارد و اجرای آن تأثیری بر اجرای سایر نخ‌ها نمی‌گذارد. این بازه زمانی توسط سیستم‌عامل تعیین می‌شود.

چند نخی به برنامه اجازه می‌دهد تا نخ‌های متعددی را به‌طور همزمان اجرا کند. این کار می‌تواند عملکرد و پاسخ‌دهی برنامه را به‌طور قابل توجهی بهبود بخشد.

تصویری از مفهوم چند نخی در سی شارپ

مثال: نمایش چند نخی در #C

// C# program to illustrate the
// concept of multithreading
using System;
using System.Threading;

public class PStore
{
    public static void method1()
    {

        for (int i = 1; i < 5; i++)
        {
            Console.WriteLine("Method1 is : {0}", i);

            // sleep for 100 milliseconds
            if (i == 2)
            {
                Thread.Sleep(100);
            }
        }
    }

    public static void method2()
    {
        // It prints numbers from 0 to 10
        for (int j = 1; j < 5; j++)
        {
            Console.WriteLine("Method2 is : {0}", j);
        }
    }
    static public void Main()
    {
        // Creating and initializing threads
        Thread thr1 = new Thread(method1);
        Thread thr2 = new Thread(method2);
        thr1.Start();
        thr2.Start();
    }
}

خروجی

Method1 is : 1
Method1 is : 2
Method2 is : 1
Method2 is : 2
Method2 is : 3
Method2 is : 4
Method1 is : 3
Method1 is : 4

توضیح: در مثال بالا، دو نخ به نام‌های thr1 و thr2 با استفاده از کلاس Thread ایجاد و مقداردهی اولیه می‌شوند. سپس با استفاده از دستورات ;()thr1.Start و ;()thr2.Start اجرای هر دو نخ آغاز می‌شود. اکنون هر دو نخ به‌طور همزمان اجرا می‌شوند و پردازش thr2 وابسته به پردازش thr1 نیست، برخلاف مدل تک‌نخی.

توجه: خروجی ممکن است به دلیل تغییر زمینه (context-switching) متفاوت باشد.

روش‌های پیاده‌سازی چند نخی

در #C، چند نخی از طریق فضای نام System.Threading پشتیبانی می‌شود. چندین روش برای ایجاد و مدیریت نخ‌ها وجود دارد که در زیر توضیح داده شده است:

  1. کلاس Thread
  2. کلاس Task
  3. Async و Await
  4. کلاس ThreadPool

۱- کلاس Thread

کلاس Thread ساده‌ترین روش برای پیاده‌سازی چند نخی در سی شارپ #C است. می‌توانیم یک نخ با ایجاد یک شی از کلاس Thread بسازیم و متدی که نمایانگر وظیفه‌ای است که باید اجرا شود را به آن پاس دهیم.

مثال: ایجاد نخ‌ها با استفاده از کلاس Thread

// Implementing Multithreading using Thread class
using System;
using System.Threading;

class PStore
{
    static void Main()
    {
        Thread thread1 = new Thread(PrintNumbers);
        
        // Start the thread
        thread1.Start();

        PrintNumbers();
    }

    static void PrintNumbers()
    {
        for (int i = 1; i < 5; i++)
        {
            Console.WriteLine(i);
            Thread.Sleep(100);
        }
    }
}

خروجی

۱
۱
۲
۲
۳
۳
۴
۴

توضیح: در مثال بالا، متد PrintNumbers در هر دو نخ، نخ اصلی و نخ جداگانه thread1 اجرا می‌شود. متد ()Start اجرای نخ را آغاز می‌کند. این نخ با ایجاد وقفه برای چند ثانیه بین هر تکرار، شبیه‌سازی کارهایی را انجام می‌دهد.

۲- کلاس Task

کلاس Task برای وظایف پیچیده‌تر یا موازی استفاده می‌شود که به آن‌ها کتابخانه موازی‌سازی وظایف (Task Parallel Library یا TPL) نیز گفته می‌شود. کلاس Task به ما این امکان را می‌دهد تا وظایفی ایجاد کنیم که به‌صورت غیرهمزمان اجرا شوند، که هم عملکرد را بهبود می‌بخشد و هم خوانایی کد را افزایش می‌دهد. این کلاس همچنین مدیریت نخ‌ها را با استفاده از استخر نخ‌ها «thread pooling» و هماهنگ‌سازی «synchronization» ساده‌تر می‌کند.

مثال:

// Implmentation of Multithreading in C# using Task Class
using System;
using System.Threading.Tasks;

class PStore
{
    static void Main()
    {
        // Running two tasks concurrently
        Task task1 = Task.Run(() => PrintNumbers());
        Task task2 = Task.Run(() => PrintNumbers());

        // Wait for both tasks to complete
        Task.WhenAll(task1, task2).Wait();
    }

    static void PrintNumbers()
    {
        for (int i = 0; i < 3; i++)
        {
            Console.WriteLine(i);
        }
    }
}

خروجی

۰
۱
۲
۰
۱
۲

توضیح: در مثال بالا، دو وظیفه (Task) با استفاده از ()Task.Run ایجاد شده‌اند. این وظایف به‌طور همزمان متد PrintNumbers را اجرا می‌کنند که باعث بهبود عملکرد کلی می‌شود. متد ()Task.WhenAll اطمینان حاصل می‌کند که هر دو وظیفه پیش از پایان اجرای برنامه به اتمام رسیده‌اند.

۳- Async و Await

روش دیگری برای انجام چندوظیفگی استفاده از وظایف غیرهمزمان «asynchronous tasks» با استفاده از کلمات کلیدی async و await است. این دو برای برنامه‌نویسی غیرهمزمان به‌کار می‌روند و می‌توانند عملیات‌هایی که به پردازنده «CPU-bound» یا ورودی/خروجی «I/O-bound» وابسته هستند را با هم ترکیب کنند. این روش چند نخی سنتی محسوب نمی‌شود اما می‌توان آن را با Task.Run ترکیب کرد تا عملیات چند نخی غیرمسدودکننده «non-blocking» انجام شود.

مثال: در تکه کد زیر، ترکیب وظایف با استفاده از async/await برای انجام عملیات‌های غیرهمزمان «asynchronous» در برنامه‌ها نمایش داده شده است. در ابتدا، از نخ‌ها «Threads» برای اجرای دو وظیفه به‌طور همزمان استفاده می‌شود و سپس از Task و async/await برای انجام همان وظایف به‌صورت غیرهمزمان استفاده می‌شود.

// Combining tasks with async/await to perform 
// asynchronous operations
using System;
using System.Threading;
using System.Threading.Tasks;

class PStore
{
    static void Main()
    {
        // Example using Threads
        Thread thread1 = new Thread(() => task("Thread 1"));
        Thread thread2 = new Thread(() => task("Thread 2"));
        thread1.Start();
        thread2.Start();
        thread1.Join();
        thread2.Join();

        Console.WriteLine("moving to task");

        // Example using Tasks with async/await
        Task.Run(async () => await RunAsyncTasks()).Wait();

        Console.WriteLine("All tasks completed.");
    }

    static void task(string threadName)
    {
        for (int i = 1; i <= 2; i++)
        {
            Console.WriteLine($"{threadName} print: {i}");
            Thread.Sleep(100); // Simulate work
        }
    }

    static async Task RunAsyncTasks()
    {
        Task task1 = Task.Run(() => task("Task 1"));
        Task task2 = Task.Run(() => task("Task 2"));

        await Task.WhenAll(task1, task2);
    }
}

خروجی

Thread 1 print: 1
Thread 2 print: 1
Thread 1 print: 2
Thread 2 print: 2
moving to task
Task 2 print: 1
Task 2 print: 2
Task 1 print: 1
Task 1 print: 2
All tasks completed.

توضیح: در مثال بالا ابتدا، پیام‌های Thread 1 و Thread 2 چاپ می‌شود، که نشان‌دهنده اجرای نخ‌ها به‌طور همزمان است. سپس، پیام‌ها برای وظایف Task 1 و Task 2 چاپ می‌شود که به‌طور غیرهمزمان اجرا شده‌اند.

در نهایت، پس از اتمام تمام وظایف، پیام “.All tasks completed” چاپ می‌شود.

۴- کلاس ThreadPool

کلاس ThreadPool یکی از ویژگی‌های کاربردی #C است. این کلاس یک مجموعه (pool) از نخ‌ها را مدیریت می‌کند و می‌توان از آن برای اجرای وظایف به‌صورت غیرهمزمان استفاده کرد. به‌جای ایجاد و نابودی نخ برای هر وظیفه که می‌تواند منابع زیادی مصرف کند، ThreadPool این امکان را فراهم می‌سازد که چندین وظیفه از تعداد محدودی نخ به‌صورت مشترک استفاده کنند.

این رویکرد باعث کاهش سربار (overhead) و افزایش عملکرد می‌شود، به‌ویژه در برنامه‌هایی که نیاز به عملیات‌های مکرر و کوتاه‌مدت دارند.

مثال: نمایش استفاده از کلاس ThreadPool در #C

// Use of ThreadPool Class
using System;
using System.Threading;

class PStore
{
    static void Main()
    {
        // queue a work item to the thread pool
        ThreadPool.QueueUserWorkItem(Worker, "Hello, world!");

        // do some other work in the main thread
        for (int i = 1; i < 5; i++)
        {
            Console.WriteLine("Main thread doing some work");
            Thread.Sleep(100);
        }

        Console.WriteLine("Done");
    }

    static void Worker(object state)
    {
        string message = (string)state;

        for (int i = 1; i < 5; i++)
        {
            Console.WriteLine(message);
            Thread.Sleep(100);
        }
    }
}

خروجی

Main thread doing some work
Hello, world!
Hello, world!
Main thread doing some work
Main thread doing some work
Hello, world!
Hello, world!
Main thread doing some work
Done

توضیح: در مثال بالا، متد Worker در یک نخ جداگانه اجرا می‌شود، در حالی که نخ اصلی در حال انجام کار دیگری است. متد Thread.Sleep برای شبیه‌سازی انجام برخی کارها در هر دو نخ استفاده شده است.

نکات مهم:

  • بن‌بست «Deadlocks»: اطمینان حاصل کنید که نخ‌ها در انتظار منابعی که توسط نخ دیگر نگه داشته شده‌اند، گیر نکنند. از کلیدواژه lock با احتیاط استفاده کنید و منابع را به ترتیبی ثابت قفل کنید.
  • استفاده از استخر نخ‌ها «Thread Pooling»: از ()Task.Run و ThreadPool استفاده کنید تا نیازی به مدیریت دستی نخ‌ها نباشد. استفاده از استخر نخ باعث کاهش سربار و بهبود بهره‌وری منابع می‌شود.
  • محدودیت زمانی: همیشه مکانیزمی برای لغو وظایف طولانی در نظر بگیرید تا پاسخ‌گویی برنامه (به‌ویژه در رابط‌های کاربری) حفظ شود.
  • داده‌های مشترک: استفاده از داده‌های مشترک بین نخ‌ها را به حداقل برسانید تا از شرایط مسابقه (Race Conditions) جلوگیری شود. در صورت نیاز به استفاده از داده‌های مشترک، حتماً از همگام‌سازی مناسب استفاده کنید.

مزایای چند نخی در سی شارپ

از مزایای استفاده از چند نخی (Multithreading) در زبان برنامه‌نویسی سی‌شارپ عبارتند از:

  • اجرای چندین فرآیند به‌صورت همزمان
  • استفاده حداکثری از منابع CPU
  • اشتراک‌گذاری زمانی بین فرآیندهای مختلف
  • کمک به دستیابی به چندوظیفگی «Multitasking»

در مجموع، استفاده از چند نخی در سی‌شارپ باعث بهبود کارایی و مقیاس‌پذیری برنامه‌ها، کاهش زمان تأخیر، و مدیریت بهتر منابع می‌شود. این ویژگی به‌ویژه در برنامه‌های پیچیده و با حجم بالای داده‌ها، مانند برنامه‌های شبکه‌ای یا سیستم‌های پردازش داده‌های بزرگ، اهمیت زیادی دارد.

نتیجه‌گیری

چند نخی یکی از تکنیک‌های قدرتمند در برنامه‌نویسی #C است که با استفاده صحیح از آن می‌توان کارایی، سرعت، و پاسخ‌گویی برنامه‌ها را به‌طور چشم‌گیری افزایش داد. این قابلیت به توسعه‌دهندگان اجازه می‌دهد تا وظایف مختلف را به‌صورت همزمان اجرا کنند و از منابع سیستم به‌ویژه CPU حداکثر بهره‌برداری را داشته باشند.

در این مقاله با مفاهیم پایه‌ای چند نخی در سی شارپ آشنا شدیم و روش‌های مختلف پیاده‌سازی آن از جمله استفاده از کلاس‌های Thread، Task، کلمات کلیدی async و await و همچنین ThreadPool را بررسی کردیم. همچنین نکات مهمی همچون مدیریت داده‌های مشترک، جلوگیری از بن‌بست، و استفاده صحیح از منابع نیز مطرح شد که برای توسعه‌ی برنامه‌های پایدار و کارآمد حیاتی هستند.

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


سوالات متداول


چند نخی در سی شارپ #C چیست؟

چند نخی به تکنیکی گفته می‌شود که در آن چندین نخ به‌طور همزمان در یک برنامه اجرا می‌شوند. این امکان به برنامه‌ها اجازه می‌دهد تا چندین وظیفه را به‌طور موازی انجام دهند، که باعث بهبود عملکرد و کارایی می‌شود.

تفاوت بین کلاس‌های Thread و Task چیست؟

کلاس Thread برای مدیریت نخ‌های سطح پایین‌تر و اجرای وظایف به‌صورت مستقیم استفاده می‌شود، در حالی که Task برای انجام وظایف پیچیده‌تر یا موازی به‌کار می‌رود و از کتابخانه Task Parallel Library (TPL) پشتیبانی می‌کند. Task مزایایی از جمله مدیریت بهتر نخ‌ها و همگام‌سازی را فراهم می‌کند.

آیا استفاده از چند نخی در همه برنامه‌ها ضروری است؟

خیر. استفاده از چند نخی بستگی به نیاز برنامه دارد. در برنامه‌هایی که نیاز به پردازش‌های هم‌زمان یا زمان‌بر دارند، مانند برنامه‌های وب یا بازی‌های چندنفره، چند نخی می‌تواند بسیار مفید باشد. اما در برنامه‌های ساده‌تر یا آن‌هایی که نیاز به پردازش موازی ندارند، ممکن است استفاده از آن ضروری نباشد.

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

در صورتی که نیاز به استفاده از داده‌های مشترک بین نخ‌ها باشد، باید از همگام‌سازی مناسب استفاده کرد. از ابزارهایی مانند lock، Mutex و Semaphore می‌توان برای جلوگیری از شرایط مسابقه (race condition) و اطمینان از همگام‌سازی داده‌ها استفاده کرد.

چه زمانی باید از کلمات کلیدی async و await استفاده کرد؟

کلمات کلیدی async و await برای اجرای وظایف غیرهمزمان (asynchronous) به‌کار می‌روند و معمولاً برای کار با عملیات‌هایی که زمان‌بر هستند، مانند خواندن از فایل یا ارسال درخواست‌های شبکه‌ای استفاده می‌شوند. این کلمات کلیدی به شما این امکان را می‌دهند که بدون مسدود کردن نخ اصلی، وظایف را اجرا کنید.

میزان رضایتمندی
لطفاً میزان رضایت خودتان را از این مطلب با دادن امتیاز اعلام کنید.
[ امتیاز میانگین 5 از 4 نفر ]
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع و مراجع:
tpointtech geeksforgeeks مجله پی استور

دیدگاه‌ خود را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *



برچسب‌ها:
سی شارپ


پیمایش به بالا