021-88881776

آموزش برنامه‌نویسی موازی و چندنخی در .NET

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

مفاهیم برنامه‌نویسی موازی

برنامه‌نویسی موازی و چندنخی در .NET یکی از مهم‌ترین مهارت‌هایی است که هر توسعه‌دهنده‌ای باید با آن آشنا باشد، به‌ویژه وقتی هدفش ساخت برنامه‌هایی با عملکرد بالا و استفاده بهینه از منابع سیستم باشد. این بخش به شما کمک می‌کند تا درک عمیق‌تری از مفهوم اجرای همزمان کارها پیدا کنید، چه از نظر تئوری و چه از نظر کاربردی. در ادامه، مفاهیم را با جزئیات بیشتری توضیح می‌دهم، مثال‌های متنوع‌تری ارائه می‌کنم و جنبه‌های مختلف را با زبانی ساده و قابل فهم برای مبتدیان بررسی می‌کنم.

اجرای همزمان چیست؟

در هسته‌ی برنامه‌نویسی موازی و چندنخی در .NET، ایده‌ی اصلی این است که چندین کار یا فرآیند به صورت همزمان (یا حداقل به نظر همزمان) اجرا شوند. اما این “همزمانی” بسته به سخت‌افزار و نوع پیاده‌سازی می‌تواند معانی مختلفی داشته باشد. بیایید این موضوع را قدم به قدم باز کنیم:

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

موازی‌کاری (Parallelism) در مقابل چندنخی (Multithreading)

برای اینکه بتوانید از برنامه‌نویسی موازی و چندنخی در .NET به بهترین شکل استفاده کنید، باید تفاوت این دو مفهوم را به صورت عمیق درک کنید:

موازی‌کاری (Parallelism):

تعریف: وقتی چندین کار به صورت واقعی و همزمان روی هسته‌های مختلف یک پردازنده چند هسته‌ای اجرا می‌شوند.
شرط اصلی: نیاز به سخت‌افزار چند هسته‌ای دارد (مثل CPUهای مدرن که 2، 4، 8 یا بیشتر هسته دارند).
کاربرد: مناسب برای کارهایی که می‌توانند به بخش‌های مستقل تقسیم شوند و هر بخش روی یک هسته جداگانه اجرا شود.
مثال: فرض کنید یک ویدئو را رندر می‌کنید. می‌توانید فریم‌های مختلف را به هسته‌های مختلف پردازنده اختصاص دهید تا همزمان پردازش شوند.
مزیت: سرعت اجرای واقعی بیشتر می‌شود، چون چند هسته به طور فیزیکی کار را تقسیم می‌کنند.

چندنخی (Multithreading):

تعریف: وقتی چندین نخ (Thread) در یک برنامه ایجاد می‌شوند تا کارهای مختلف را انجام دهند، حتی اگر فقط یک هسته پردازنده در دسترس باشد. در این حالت، سیستم‌عامل بین نخ‌ها جابه‌جا می‌شود (Context Switching).
شرط اصلی: نیازی به چند هسته ندارد؛ حتی روی یک پردازنده تک‌هسته‌ای هم کار می‌کند.
کاربرد: برای کارهایی که منتظر چیزی هستند (مثل دانلود فایل یا ورودی کاربر) یا وقتی می‌خواهید برنامه پاسخگو بماند.
مثال: در یک برنامه چت، یک نخ پیام‌ها را دریافت می‌کند، در حالی که نخ دیگر رابط کاربری را مدیریت می‌کند.
مزیت: پاسخگویی برنامه بیشتر می‌شود، حتی اگر سرعت واقعی افزایش پیدا نکند.
مثال تصویری: تصور کنید یک جاده دارید. در موازی‌کاری، چند خط موازی وجود دارد و ماشین‌ها (کارها) همزمان در خطوط مختلف حرکت می‌کنند. در چندنخی، یک خط وجود دارد و ماشین‌ها به نوبت و سریع جابه‌جا می‌شوند.

چگونه این مفاهیم در .NET پیاده‌سازی می‌شوند؟

در برنامه‌نویسی موازی و چندنخی در .NET، این دو مفهوم با ابزارهایی مثل کلاس Thread، کتابخانه Task Parallel Library (TPL) و ساختارهایی مثل Parallel.For پیاده‌سازی می‌شوند. هدف این ابزارها این است که توسعه‌دهندگان بتوانند به سادگی از توان پردازشی سیستم استفاده کنند.

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

انواع مسائل قابل حل با برنامه‌نویسی موازی

برای اینکه بهتر بفهمید چه زمانی باید از برنامه‌نویسی موازی و چندنخی در .NET استفاده کنید، بیایید چند سناریوی واقعی را بررسی کنیم:

مسائل محاسباتی (CPU-Bound):

وقتی برنامه شما نیاز به پردازش سنگین دارد، مثل مرتب‌سازی یک لیست بزرگ، محاسبه اعداد اول یا تبدیل فایل‌های بزرگ.
مثال عملی: فرض کنید باید یک آرایه 1 میلیونی از اعداد را جمع کنید. به جای جمع کردن تک‌تک اعداد با یک نخ، می‌توانید آرایه را به 4 بخش تقسیم کنید و هر بخش را به یک هسته بدهید.

مسائل ورودی/خروجی (I/O-Bound):

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

برنامه‌های تعاملی:

وقتی می‌خواهید رابط کاربری (UI) برنامه‌تان در حین انجام کارهای سنگین فریز نشود.
مثال عملی: در یک اپلیکیشن دسکتاپ، یک نخ داده‌ها را از دیتابیس می‌خواند و نخ دیگر رابط کاربری را به‌روز نگه می‌دارد.

یک مثال برنامه‌نویسی ساده

فرض کنید می‌خواهید دو کار را همزمان انجام دهید: چاپ اعداد 1 تا 10 و حروف A تا J. بدون موازی‌کاری یا چندنخی، این کارها پشت سر هم انجام می‌شوند:

for (int i = 1; i <= 10; i++) Console.Write(i + " ");
for (char c = 'A'; c <= 'J'; c++) Console.Write(c + " ");

خروجی: 1 2 3 4 5 6 7 8 9 10 A B C D E F G H I J

اما با برنامه‌نویسی موازی و چندنخی در .NET، می‌توانید آن‌ها را همزمان اجرا کنید:

using System.Threading;

class Program
{
    static void Main()
    {
        Thread numbers = new Thread(() => { for (int i = 1; i <= 10; i++) Console.Write(i + " "); });
        Thread letters = new Thread(() => { for (char c = 'A'; c <= 'J'; c++) Console.Write(c + " "); });
        numbers.Start();
        letters.Start();
    }
}

خروجی احتمالی: 1 A 2 B 3 C 4 D 5 E 6 F 7 G 8 H 9 I 10 J

توضیح: چون دو نخ همزمان کار می‌کنند، خروجی ممکن است در هم آمیخته شود، اما کارها سریع‌تر انجام می‌شوند.

مزایا و معایب

مزایا:

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

معایب:

پیچیدگی بیشتر در کدنویسی (مثل مدیریت همزمانی که بعداً توضیح می‌دهیم).
سربار (Overhead) ایجاد نخ‌ها که ممکن است برای کارهای کوچک مقرون‌به‌صرفه نباشد.

چه زمانی نباید از آن استفاده کنیم؟

وقتی کارها خیلی کوچک و ساده هستند (مثل جمع 10 عدد).
وقتی منابع مشترک زیادی دارید و هماهنگی بین نخ‌ها سخت می‌شود.

استفاده از Task و Thread

در برنامه‌نویسی موازی و چندنخی در .NET، دو ابزار اصلی برای مدیریت نخ‌ها و اجرای همزمان کارها وجود دارد: Thread و Task. این دو ابزار هر کدام ویژگی‌ها، مزایا و معایب خاص خود را دارند و انتخاب بین آن‌ها بستگی به نیاز پروژه و سطح کنترل مورد نظر شما دارد. در این بخش، به صورت جامع و با جزئیات، این دو مفهوم را توضیح می‌دهم، مثال‌های متنوع ارائه می‌کنم و به شما کمک می‌کنم تا بفهمید چه زمانی از کدام استفاده کنید. اگر مبتدی هستید، نگران نباشید؛ همه چیز را با زبانی ساده و گام‌به‌گام توضیح می‌دهم.

Thread: واحد پایه اجرای کد

Thread یا “نخ”، کوچک‌ترین واحد اجرایی در یک برنامه است که توسط سیستم‌عامل مدیریت می‌شود. در .NET، کلاس Thread به شما این امکان را می‌دهد که یک کار خاص را به صورت جداگانه و مستقل از نخ اصلی برنامه اجرا کنید. این روش از روزهای ابتدایی برنامه‌نویسی شیءگرا وجود داشته و هنوز هم در سناریوهای خاص کاربرد دارد.

چگونه کار می‌کند؟

وقتی یک برنامه .NET را اجرا می‌کنید، به طور پیش‌فرض روی یک نخ اصلی (Main Thread) شروع به کار می‌کند. با استفاده از کلاس Thread، می‌توانید نخ‌های اضافی بسازید تا کارهای دیگر را به صورت همزمان انجام دهند.

مثال ساده:

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Thread thread = new Thread(() => 
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine($"نخ جدید: {i}");
                Thread.Sleep(500); // 500 میلی‌ثانیه تاخیر
            }
        });
        thread.Start();

        Console.WriteLine("نخ اصلی در حال اجرا است.");
    }
}

خروجی احتمالی:

نخ اصلی در حال اجرا است.
نخ جدید: 0
نخ جدید: 1
نخ جدید: 2
نخ جدید: 3
نخ جدید: 4

توضیح:

یک نخ جدید ایجاد شده و اعداد 0 تا 4 را با تاخیر چاپ می‌کند.
نخ اصلی همزمان پیام خودش را چاپ می‌کند و منتظر نخ جدید نمی‌ماند.

مزایا

کنترل کامل: شما تصمیم می‌گیرید نخ کی شروع شود، کی متوقف شود و چگونه اجرا شود.
مناسب برای سناریوهای خاص: مثلاً وقتی نیاز به اولویت‌بندی نخ‌ها دارید (با Thread.Priority).

معایب

مدیریت دستی: باید خودتان شروع و پایان نخ‌ها را کنترل کنید، که می‌تواند پیچیده شود.
سربار زیاد: ایجاد هر نخ هزینه پردازشی دارد و اگر تعداد زیادی نخ بسازید، عملکرد سیستم کاهش می‌یابد.
عدم بهینه‌سازی خودکار: برخلاف Task، سیستم نمی‌تواند منابع را خودش مدیریت کند.

موارد استفاده

وقتی نیاز به کنترل دقیق روی رفتار نخ دارید.
وقتی با کد قدیمی کار می‌کنید که از Thread استفاده کرده است.

Task: رویکرد مدرن و هوشمند

Task یک ابزار پیشرفته‌تر برای برنامه‌نویسی موازی و چندنخی در .NET است که از کتابخانه Task Parallel Library (TPL) استفاده می‌کند. این روش در نسخه‌های جدید .NET معرفی شده و هدفش ساده‌تر کردن کار با نخ‌ها و بهینه‌سازی منابع است. برخلاف Thread که شما را مجبور به مدیریت دستی می‌کند، Task مثل یک دستیار هوشمند عمل می‌کند و بسیاری از جزئیات را خودش مدیریت می‌کند.

چگونه کار می‌کند؟

Task یک “وظیفه” را تعریف می‌کند که می‌تواند به صورت ناهمگام (Asynchronous) یا موازی اجرا شود. .NET از یک Thread Pool (استخر نخ‌ها) استفاده می‌کند تا به جای ایجاد نخ جدید برای هر کار، از نخ‌های موجود دوباره استفاده کند. این کار باعث کاهش سربار و افزایش کارایی می‌شود.

مثال ساده:

using System;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        Task task = Task.Run(() => 
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine($"تسک: {i}");
                Task.Delay(500).Wait(); // تاخیر 500 میلی‌ثانیه
            }
        });

        Console.WriteLine("نخ اصلی در حال اجرا است.");
        task.Wait(); // منتظر اتمام تسک
    }
}

خروجی:

نخ اصلی در حال اجرا است.
تسک: 0
تسک: 1
تسک: 2
تسک: 3
تسک: 4

توضیح:

Task.Run یک کار را در پس‌زمینه اجرا می‌کند.
task.Wait() باعث می‌شود نخ اصلی منتظر اتمام تسک بماند (اختیاری است).

مزایا

سادگی: نیازی به مدیریت دستی نخ‌ها ندارید.
بهینه‌سازی: از Thread Pool استفاده می‌کند و تعداد نخ‌ها را به صورت هوشمند تنظیم می‌کند.
انعطاف‌پذیری: امکان استفاده از async/await، ترکیب چندین تسک و مدیریت خطاها وجود دارد.
عملکرد بهتر: برای کارهای طولانی‌مدت و موازی بسیار کارآمد است.

معایب

کنترل کمتر: جزئیات اجرای نخ‌ها از شما پنهان است.
پیچیدگی در سناریوهای خاص: اگر نیاز به تنظیمات خیلی خاص داشته باشید، ممکن است Thread مناسب‌تر باشد.

موارد استفاده

کارهای طولانی‌مدت مثل پردازش داده یا دانلود فایل.
برنامه‌هایی که نیاز به پاسخگویی بالا دارند (مثل UI).
پروژه‌های مدرن که از async/await بهره می‌برند.

مقایسه عملی Thread و Task

بیایید یک مثال مقایسه‌ای ببینیم که هر دو را در یک سناریو تست کنیم: فرض کنید می‌خواهیم دو کار همزمان انجام دهیم (چاپ اعداد و حروف).

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Thread numbers = new Thread(() => 
        {
            for (int i = 1; i <= 5; i++) Console.WriteLine($"عدد: {i}");
        });
        Thread letters = new Thread(() => 
        {
            for (char c = 'A'; c <= 'E'; c++) Console.WriteLine($"حرف: {c}");
        });

        numbers.Start();
        letters.Start();

        numbers.Join(); // منتظر اتمام نخ اعداد
        letters.Join(); // منتظر اتمام نخ حروف
    }
}

خروجی احتمالی:

عدد: 1
حرف: A
عدد: 2
حرف: B
عدد: 3
حرف: C
عدد: 4
حرف: D
عدد: 5
حرف: E

با Task

using System;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        Task numbers = Task.Run(() => 
        {
            for (int i = 1; i <= 5; i++) Console.WriteLine($"عدد: {i}");
        });
        Task letters = Task.Run(() => 
        {
            for (char c = 'A'; c <= 'E'; c++) Console.WriteLine($"حرف: {c}");
        });

        Task.WaitAll(numbers, letters); // منتظر اتمام همه تسک‌ها
    }
}

خروجی احتمالی: مشابه حالت Thread است، اما اجرای آن بهینه‌تر است.

تفاوت‌ها در عمل

Thread: شما دو نخ جداگانه می‌سازید و سیستم باید آن‌ها را از صفر ایجاد کند.
Task: از نخ‌های موجود در Thread Pool استفاده می‌کند و سربار کمتری دارد.
مدیریت: در Thread باید از Join استفاده کنید، اما Task با WaitAll ساده‌تر مدیریت می‌شود.

چه زمانی از کدام استفاده کنیم؟

Thread:

وقتی نیاز به کنترل کامل دارید (مثل تغییر اولویت یا توقف دستی).
برای کارهای کوتاه و ساده که نیازی به بهینه‌سازی پیچیده ندارند.

Task:

برای پروژه‌های مدرن و پیچیده.
وقتی می‌خواهید از async/await یا موازی‌کاری واقعی (با Parallel) استفاده کنید.
برای کارهای طولانی که نیاز به مدیریت هوشمند منابع دارند.

نکته پیشرفته: ترکیب Task با async/await

در برنامه‌نویسی موازی و چندنخی در .NET، می‌توانید Task را با کلمات کلیدی async و await ترکیب کنید تا کد خواناتر و ناهمگام شود:

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        await Task.Run(() => Console.WriteLine("تسک در حال اجرا است!"));
        Console.WriteLine("نخ اصلی ادامه داد.");
    }
}

با این توضیحات، حالا می‌توانید با اطمینان از Thread و Task در پروژه‌هایتان استفاده کنید و تفاوت‌هایشان را در عمل ببینید!

مدیریت همزمانی

در برنامه‌نویسی موازی و چندنخی در .NET، وقتی چندین نخ (Thread) یا تسک (Task) به صورت همزمان اجرا می‌شوند، ممکن است با چالش‌هایی روبه‌رو شوید که به هماهنگی و مدیریت دقیق نیاز دارند. این چالش‌ها معمولاً زمانی رخ می‌دهند که چند نخ سعی می‌کنند به یک منبع مشترک (مثل یک متغیر، فایل یا دیتابیس) دسترسی پیدا کنند. بدون مدیریت صحیح، این موقعیت‌ها می‌توانند به مشکلاتی مثل وضعیت رقابتی (Race Condition)، قفل مرده (Deadlock) یا حتی خرابی برنامه منجر شوند. در این بخش، به صورت جامع و با جزئیات، ابزارها، تکنیک‌ها و راه‌حل‌های مدیریت همزمانی را توضیح می‌دهم تا بتوانید برنامه‌های موازی خود را ایمن و کارآمد نگه دارید.

چرا مدیریت همزمانی مهم است؟

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

مثال ساده: فرض کنید یک حساب بانکی با موجودی 1000 تومان دارید. دو نفر (دو نخ) همزمان می‌خواهند 500 تومان برداشت کنند. اگر همزمانی مدیریت نشود، ممکن است هر دو برداشت موفق شوند و موجودی به -500 تومان برسد، در حالی که باید یکی از برداشت‌ها رد شود!

مشکلات رایج در همزمانی

قبل از بررسی ابزارها، بیایید مشکلات اصلی را بشناسیم:

وضعیت رقابتی (Race Condition):

چیست؟: وقتی نتیجه اجرای برنامه به ترتیب اجرای نخ‌ها بستگی دارد و این ترتیب غیرقابل پیش‌بینی است.
مثال: دو نخ همزمان یک متغیر counter را افزایش می‌دهند. اگر هر دو مقدار فعلی را بخوانند (مثلاً 5)، آن را افزایش دهند و بنویسند (6)، نتیجه نهایی 6 می‌شود، در حالی که باید 7 باشد.
راه‌حل: استفاده از قفل‌گذاری برای اطمینان از دسترسی انحصاری.

قفل مرده (Deadlock):

چیست؟: وقتی دو یا چند نخ منتظر یکدیگر می‌مانند و هیچ‌کدام نمی‌توانند ادامه دهند.
مثال: نخ A منتظر منبعی است که نخ B قفل کرده و نخ B منتظر منبعی است که نخ A قفل کرده.
راه‌حل: ترتیب مشخصی برای دسترسی به منابع تعریف کنید.

گرسنگی (Starvation):

چیست؟: وقتی یک نخ به دلیل اولویت پایین یا زمان‌بندی بد، هرگز فرصت اجرا پیدا نمی‌کند.
راه‌حل: استفاده از ابزارهایی مثل Semaphore برای مدیریت عادلانه.

ابزارهای مدیریت همزمانی در .NET

در برنامه‌نویسی موازی و چندنخی در .NET، ابزارهای مختلفی برای کنترل همزمانی وجود دارد که هر کدام برای سناریوهای خاصی طراحی شده‌اند. بیایید آن‌ها را با جزئیات بررسی کنیم:

1. قفل‌گذاری با lock

کارکرد: ساده‌ترین و رایج‌ترین روش برای اطمینان از اینکه فقط یک نخ در لحظه به یک منبع مشترک دسترسی دارد.
نحوه استفاده: از کلمه کلیدی lock همراه با یک شیء مرجع (معمولاً یک object) استفاده می‌شود.
مثال عملی:

using System;
using System.Threading.Tasks;

class Program
{
    static object lockObject = new object();
    static int counter = 0;

    static void IncrementCounter()
    {
        lock (lockObject)
        {
            counter++;
            Console.WriteLine($"مقدار: {counter}");
        }
    }

    static void Main()
    {
        Task.Run(IncrementCounter);
        Task.Run(IncrementCounter);
        Task.Run(IncrementCounter);
        Console.ReadLine(); // برای مشاهده خروجی
    }
}

خروجی:

مقدار: 1
مقدار: 2
مقدار: 3

توضیح: بدون lock، ممکن بود چند نخ همزمان counter را بخوانند و مقدار نادرستی ثبت شود. lock تضمین می‌کند که فقط یک نخ در لحظه داخل بلوک قفل‌شده باشد.

2. Mutex (Mutual Exclusion)

کارکرد: شبیه lock است، اما در سطح سیستم‌عامل عمل می‌کند و می‌تواند بین فرآیندهای مختلف (نه فقط نخ‌های یک برنامه) همگام‌سازی کند.
مثال:

using System;
using System.Threading;

class Program
{
    static Mutex mutex = new Mutex();
    static int counter = 0;

    static void IncrementCounter()
    {
        mutex.WaitOne(); // منتظر دسترسی انحصاری
        counter++;
        Console.WriteLine($"مقدار: {counter}");
        mutex.ReleaseMutex(); // آزاد کردن
    }

    static void Main()
    {
        Thread t1 = new Thread(IncrementCounter);
        Thread t2 = new Thread(IncrementCounter);
        t1.Start();
        t2.Start();
    }
}

کاربرد: وقتی نیاز به هماهنگی بین چند برنامه دارید (مثلاً دو اپلیکیشن که به یک فایل مشترک دسترسی دارند).

3. Semaphore

کارکرد: به تعداد مشخصی از نخ‌ها اجازه دسترسی به یک منبع را می‌دهد (برخلاف lock و Mutex که فقط یک نخ را مجاز می‌کنند).
مثال: محدود کردن تعداد دانلودهای همزمان:

using System;
using System.Threading;

class Program
{
    static Semaphore semaphore = new Semaphore(2, 2); // حداکثر 2 نخ همزمان
    static void DownloadFile(string fileName)
    {
        semaphore.WaitOne();
        Console.WriteLine($"{fileName} در حال دانلود...");
        Thread.Sleep(2000); // شبیه‌سازی دانلود
        Console.WriteLine($"{fileName} دانلود شد.");
        semaphore.Release();
    }

    static void Main()
    {
        Thread t1 = new Thread(() => DownloadFile("فایل 1"));
        Thread t2 = new Thread(() => DownloadFile("فایل 2"));
        Thread t3 = new Thread(() => DownloadFile("فایل 3"));
        t1.Start();
        t2.Start();
        t3.Start();
    }
}

خروجی احتمالی:

فایل 1 در حال دانلود...
فایل 2 در حال دانلود...
فایل 1 دانلود شد.
فایل 3 در حال دانلود...
فایل 2 دانلود شد.
فایل 3 دانلود شد.

توضیح: فقط 2 نخ همزمان می‌توانند اجرا شوند.

4. مجموعه‌های همزمانی (Concurrent Collections)

کارکرد: ساختارهای داده‌ای مثل ConcurrentDictionary، ConcurrentQueue و ConcurrentBag که برای دسترسی امن چندنخی طراحی شده‌اند و نیازی به قفل دستی ندارند.
مثال:

using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

class Program
{
    static ConcurrentDictionary<int, string> data = new ConcurrentDictionary<int, string>();

    static void AddData(int key, string value)
    {
        data.TryAdd(key, value);
        Console.WriteLine($"اضافه شد: {key} = {value}");
    }

    static void Main()
    {
        Task.Run(() => AddData(1, "علی"));
        Task.Run(() => AddData(2, "رضا"));
        Task.Run(() => AddData(3, "مریم"));
        Console.ReadLine();
    }
}

مزیت: ساده‌تر و سریع‌تر از قفل‌گذاری دستی.

راه‌حل مشکلات رایج

رفع Race Condition:

از lock، Mutex یا مجموعه‌های همزمانی استفاده کنید تا دسترسی به منابع مشترک کنترل شود.
نکته: همیشه محدوده قفل را کوچک نگه دارید تا عملکرد کاهش نیابد.

جلوگیری از Deadlock:

ترتیب دسترسی به منابع را ثابت کنید (مثلاً همیشه اول قفل A، بعد قفل B).
از تایم‌اوت (Timeout) در ابزارهایی مثل Mutex استفاده کنید تا نخ‌ها بی‌نهایت منتظر نمانند.

مدیریت گرسنگی:

از Semaphore یا اولویت‌بندی نخ‌ها استفاده کنید تا همه نخ‌ها شانس اجرا داشته باشند.

نکات پیشرفته

Interlocked: برای عملیات ساده مثل افزایش یا کاهش یک متغیر بدون نیاز به lock (مثلاً Interlocked.Increment(ref counter)).
async/await با Task: در برنامه‌نویسی ناهمگام، همزمانی بدون نیاز به قفل‌گذاری در بسیاری از موارد مدیریت می‌شود.

نتیجه‌گیری

برنامه‌نویسی موازی و چندنخی در .NET مهارتی قدرتمند و ضروری برای توسعه‌دهندگانی است که می‌خواهند برنامه‌هایی سریع، کارآمد و پاسخگو بسازند. با درک مفاهیم پایه مثل موازی‌کاری و چندنخی، استفاده از ابزارهایی مانند Thread برای کنترل دقیق و Task برای مدیریت هوشمند، و به‌کارگیری تکنیک‌های مدیریت همزمانی مثل lock و مجموعه‌های همزمانی، می‌توانید از حداکثر توان پردازشی سیستم استفاده کنید. این رویکرد نه تنها عملکرد برنامه‌های شما را بهبود می‌بخشد، بلکه تجربه کاربری بهتری فراهم می‌کند. اگر تازه‌کار هستید، با تمرین ساده شروع کنید و به تدریج به سراغ پروژه‌های پیچیده‌تر بروید. در نهایت، تسلط بر برنامه‌نویسی موازی و چندنخی در .NET شما را به یک برنامه‌نویس حرفه‌ای‌تر تبدیل می‌کند که آماده مواجهه با چالش‌های دنیای مدرن است.

آموزش برنامه‌نویسی موازی و چندنخی در .NET

دیدگاه های شما

دیدگاهتان را بنویسید

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