021-88881776

آموزش برنامه‌نویسی هم‌زمان و غیرهم‌زمان در سی شارپ

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

برنامه‌نویسی هم‌زمان و غیرهم‌زمان در سی شارپ

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

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

تفاوت همزمانی و غیرهمزمانی

همزمان (Concurrency): در این روش، چندین عملیات به گونه‌ای زمان‌بندی می‌شوند که به طور مداوم در حال اجرا باشند. حتی اگر عملیات‌ها دقیقا در یک لحظه بر روی چندین هسته پردازنده اجرا نشوند، اما از نظر منطقی به صورت همزمان برنامه‌نویسی می‌شوند.

موازی (Parallelism): در اینجا چندین عملیات به صورت واقعی و همزمان بر روی چندین هسته پردازنده اجرا می‌شوند. برنامه‌نویسی موازی معمولاً از توابعی مانند Parallel.For یا Parallel.ForEach استفاده می‌کند.

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

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

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

موارد استفاده عملی و مثال‌ها

مثال عملی از یک برنامه دانلود فایل:

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

کد نمونه برای دانلود فایل به صورت غیرهم‌زمان:

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        string[] urls = {
            "https://example.com/file1.zip",
            "https://example.com/file2.zip",
            "https://example.com/file3.zip"
        };

        // استفاده از Task ها برای دانلود همزمان فایل‌ها
        Task[] downloadTasks = new Task[urls.Length];
        HttpClient client = new HttpClient();

        for (int i = 0; i < urls.Length; i++)
        {
            string url = urls[i];
            downloadTasks[i] = DownloadFileAsync(client, url);
        }

        await Task.WhenAll(downloadTasks);
        Console.WriteLine("همه فایل‌ها دانلود شدند.");
    }

    static async Task DownloadFileAsync(HttpClient client, string url)
    {
        byte[] data = await client.GetByteArrayAsync(url);
        Console.WriteLine($"فایل از {url} دانلود شد، اندازه: {data.Length} بایت");
        // می‌توانید داده‌ها را در فایل ذخیره کنید
    }
}

در این مثال، دانلود فایل‌ها به صورت غیرهم‌زمان انجام می‌شود و هر فایل به محض آماده شدن، پردازش می‌شود بدون اینکه منتظر دانلود فایل قبلی بمانیم.

نکات مهم در استفاده از برنامه‌نویسی هم‌زمان و غیرهم‌زمان در سی شارپ

درک مفاهیم پایه:
برای استفاده صحیح از این تکنیک‌ها، باید با مفاهیمی مانند نخ (Thread)، Task، async/await و Parallel Programming آشنا باشید. آشنایی با این مفاهیم باعث می‌شود بتوانید کدهای امن و بهینه‌ای بنویسید.

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

تفاوت بین CPU-bound و I/O-bound:
برنامه‌هایی که پردازش‌های محاسباتی (CPU-bound) انجام می‌دهند و برنامه‌هایی که منتظر عملیات ورودی/خروجی (I/O-bound) هستند، رویکردهای متفاوتی در برنامه‌نویسی غیرهم‌زمان نیاز دارند. برای مثال، در برنامه‌های I/O-bound استفاده از async/await بسیار موثر است.

توجه به SynchronizationContext:
در برنامه‌های ویندوزی مانند WinForms یا WPF، به‌روزرسانی UI پس از پایان عملیات غیرهم‌زمان نیازمند SynchronizationContext مناسب است تا از بروز مشکلاتی مانند قفل شدن UI جلوگیری شود.

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

مفهوم برنامه‌نویسی هم‌زمان و نحوه پیاده‌سازی آن

بررسی مفاهیم اصلی

الف) برنامه‌نویسی هم‌زمان چیست؟

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

استفاده از نخ‌ها (Threads): معمولاً با ایجاد نخ‌های جداگانه (مانند استفاده از کلاس Thread یا نخ‌های استخر شده از ThreadPool) پیاده‌سازی می‌شود.
موقعیت واقعی اجرا: در برخی موارد (به خصوص در سیستم‌های چند هسته‌ای)، برخی از عملیات‌ها به صورت واقعی بر روی چند هسته اجرا می‌شوند.
مدیریت پیچیده: استفاده مستقیم از نخ‌ها می‌تواند منجر به چالش‌های مربوط به همگام‌سازی، شرایط رقابتی (Race Conditions) و بن‌بست (Deadlock) شود.

ب) برنامه‌نویسی غیرهم‌زمان چیست؟

برنامه‌نویسی غیرهم‌زمان (Asynchrony) به معنای اجرای عملیات‌های طولانی بدون مسدود کردن نخ اصلی یا نخ رابط کاربری (UI) است. در این روش، عملیات‌های I/O-bound مانند دانلود فایل، خواندن/نوشتن از پایگاه داده یا درخواست‌های شبکه در پس‌زمینه انجام می‌شوند.

ویژگی‌های کلیدی:

عدم مسدودسازی نخ اصلی: عملیات طولانی باعث قفل شدن UI یا اجرای سایر وظایف نمی‌شود.
استفاده از async/await و Task ها: با بهره‌گیری از الگوی Task-based Asynchronous Programming، می‌توانید کدی بنویسید که بسیار شبیه کد همزمان به نظر می‌رسد اما در واقع از نخ اصلی جدا می‌شود.
مدیریت ساده‌تر: اغلب مدیریت خطا و لغو عملیات (Cancellation) در این مدل راحت‌تر و خواناتر پیاده‌سازی می‌شود.

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

برای پیاده‌سازی برنامه‌نویسی هم‌زمان و غیرهم‌زمان در سی شارپ می‌توان از چندین تکنیک بهره برد:

الف) استفاده از نخ‌ها (Threads)

روش اصلی:
استفاده از کلاس Thread برای ایجاد یک نخ جدید به صورت دستی. این روش کنترل دقیقی روی شروع و پایان عملیات‌های موازی فراهم می‌کند.

مزایا:

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

معایب:

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

مثال کد استفاده از نخ‌ها:

using System;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        // ایجاد و راه‌اندازی نخ جدید برای اجرای عملیات طولانی
        Thread thread = new Thread(new ThreadStart(LongOperation));
        thread.Start();
        Console.WriteLine("عملیات اصلی ادامه دارد...");
    }

    static void LongOperation()
    {
        // شبیه‌سازی عملیات طولانی با تاخیر ۵ ثانیه‌ای
        Thread.Sleep(5000);
        Console.WriteLine("عملیات طولانی به پایان رسید.");
    }
}

در این مثال، متد LongOperation در یک نخ جداگانه اجرا می‌شود؛ بنابراین پیام “عملیات اصلی ادامه دارد…” بلافاصله چاپ می‌شود بدون اینکه منتظر پایان عملیات طولانی بماند.

ب) استفاده از Task ها

روش اصلی:
استفاده از کلاس Task و توابعی مانند Task.Run یا Task.Factory.StartNew برای اجرای عملیات‌های غیرهم‌زمان. این روش از نخ‌های استخر شده (Thread Pool) استفاده می‌کند و مدیریت آن به عهده سیستم عامل واگذار می‌شود.

مزایا:

نوشتن کد به شیوه‌ای ساده و خوانا با استفاده از کلیدواژه‌های async و await.
مدیریت خودکار نخ‌ها و منابع سیستم.
امکان ترکیب راحت با سایر عملیات غیرهم‌زمان مانند I/O-bound.
معایب:

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

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("شروع عملیات غیرهم‌زمان...");
        await Task.Run(() => {
            // شبیه‌سازی عملیات طولانی
            Task.Delay(5000).Wait();
            Console.WriteLine("عملیات طولانی در Task به پایان رسید.");
        });
        Console.WriteLine("عملیات اصلی ادامه دارد...");
    }
}

در این مثال، عملیات طولانی در یک Task اجرا شده و با استفاده از await منتظر پایان آن می‌مانیم بدون اینکه نخ اصلی مسدود شود.

ج) استفاده از Parallel Programming

روش اصلی:
استفاده از کلاس Parallel که توابعی مانند Parallel.For و Parallel.ForEach را فراهم می‌کند. این روش برای پردازش‌های موازی داده‌های بزرگ یا تکرارهای محاسباتی مناسب است.

مزایا:

اجرای خودکار توزیع بار بین هسته‌های پردازنده.
کاهش زمان پردازش در محاسبات CPU-bound.
معایب:

در کاربردهایی که نیاز به کنترل دقیق‌تر بر روی همگام‌سازی داده‌ها دارند، ممکن است استفاده از Parallel Programming چالش‌هایی ایجاد کند.
مناسب‌تر برای محاسبات سنگین CPU است تا عملیات‌های I/O-bound.

مثال کد استفاده از Parallel.For:

using System;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        int[] data = { 1, 2, 3, 4, 5 };

        // اجرای موازی بر روی آرایه داده‌ها
        Parallel.For(0, data.Length, i =>
        {
            Console.WriteLine($"عدد: {data[i]} در نخ: {Task.CurrentId}");
        });

        Console.WriteLine("پردازش موازی به پایان رسید.");
    }
}

در این مثال، هر عنصر از آرایه data به صورت موازی پردازش شده و نتیجه در خروجی چاپ می‌شود.

 نکات مهم در پیاده‌سازی

همگام‌سازی (Synchronization):

هنگام استفاده از نخ‌ها یا Task ها، باید مراقب به‌روز‌رسانی‌های همزمان داده‌های مشترک باشید. استفاده از قفل‌ها (lock)، Monitor، یا سازوکارهای همگام‌سازی دیگر برای جلوگیری از شرایط رقابتی الزامی است.

مدیریت استثنا:

در محیط‌های هم‌زمان و غیرهم‌زمان، به دلیل اجرای موازی عملیات‌ها، مدیریت خطاها از اهمیت ویژه‌ای برخوردار است. استفاده از بلوک‌های try-catch و بررسی استثناءهای رخ داده در هر نخ یا Task ضروری است.

لغو عملیات (Cancellation):

در بسیاری از سناریوهای عملی، نیاز است عملیات‌های در حال اجرا را در صورت نیاز لغو کنید. استفاده از CancellationToken در Task ها و عملیات‌های غیرهم‌زمان راهکاری موثر برای مدیریت لغو عملیات‌ها فراهم می‌کند.

در این بخش به بررسی دقیق مفاهیم برنامه‌نویسی هم‌زمان و غیرهم‌زمان در سی شارپ پرداختیم و تفاوت‌های اساسی بین این دو رویکرد را بررسی نمودیم:

هم‌زمانی معمولاً به معنای استفاده از نخ‌ها برای اجرای موازی عملیات‌ها است.
غیرهم‌زمانی با استفاده از الگوی Task-based و کلیدواژه‌های async/await، امکان اجرای عملیات‌های طولانی بدون مسدود کردن نخ اصلی را فراهم می‌کند.
با انتخاب روش مناسب و رعایت نکات مربوط به همگام‌سازی، مدیریت استثنا و لغو عملیات، می‌توانید برنامه‌های پویاتر، پاسخگوتر و کارآمدتری در سی شارپ توسعه دهید. این تکنیک‌ها به ویژه در برنامه‌هایی که نیاز به پردازش‌های طولانی، درخواست‌های شبکه یا عملیات I/O دارند، اهمیت ویژه‌ای پیدا می‌کنند.

آشنایی با کلیدواژه‌های async و await

معرفی و اهمیت async و await

یکی از بزرگ‌ترین پیشرفت‌ها در توسعه نرم‌افزارهای مدرن، معرفی الگوی Task-based Asynchronous Programming به همراه کلیدواژه‌های async و await در سی شارپ است. این کلیدواژه‌ها به توسعه‌دهندگان این امکان را می‌دهند که کدهای غیرهم‌زمانی بنویسند که به ساختار و خوانایی کدهای هم‌زمان شباهت دارند. به عبارت دیگر، با استفاده از این کلیدواژه‌ها، می‌توان از پیچیدگی‌های معمول مرتبط با مدیریت نخ‌ها (Threads) و Callback ها اجتناب کرد.

کلیدواژه async چیست؟

تعریف:
کلیدواژه async قبل از تعریف متد قرار می‌گیرد تا به کامپایلر اعلام کند که این متد شامل کدهای غیرهم‌زمان است. با علامت‌گذاری یک متد با async، شما می‌توانید در آن از کلیدواژه await استفاده کنید.

ویژگی‌ها و نکات مهم:

نوع بازگشتی:
متدهای async معمولاً از نوع Task یا Task<T> برگردانده می‌شوند. در مواردی که متد نیازی به برگرداندن نتیجه نداشته باشد، می‌توان از Task استفاده کرد؛ و اگر نتیجه‌ای نیاز است، از Task<T> بهره برد.
اجرای همزمان یا متوالی:
لازم به ذکر است که علامت‌گذاری متد با async به تنهایی باعث اجرای آن به صورت هم‌زمان نمی‌شود؛ بلکه تنها اجازه استفاده از await را در داخل آن فراهم می‌کند. عملکرد واقعی متد بستگی به وجود یک عملیات غیرهم‌زمان (مانند فراخوانی Task.Delay، خواندن فایل به صورت غیرهم‌زمان و …) دارد.
خوانایی کد:
استفاده از async باعث می‌شود که ساختار کد شبیه به کد هم‌زمان باشد و در نتیجه درک و نگهداری آن آسان‌تر گردد.

کلیدواژه await چیست؟

تعریف:
کلیدواژه await در داخل متدهای علامت‌گذاری شده با async به کار می‌رود. وقتی که به یک عملیات غیرهم‌زمان اعمال می‌شود، اجرای کد در نقطه‌ی فراخوانی await متوقف می‌شود تا زمانی که عملیات غیرهم‌زمان تکمیل شود. در این بین، نخ فعلی (مثلاً نخ UI یا نخ اصلی) مسدود نمی‌شود و می‌تواند به کارهای دیگر بپردازد.

ویژگی‌ها و نکات مهم:

عدم مسدودسازی نخ:
وقتی از await استفاده می‌کنید، نخ جاری آزاد می‌شود تا بتواند به سایر وظایف پاسخ دهد. پس از تکمیل عملیات غیرهم‌زمان، اجرای کد از همان نقطه ادامه پیدا می‌کند.
انتقال کنترل:
در هنگام استفاده از await، کنترل به فراخواننده (caller) باز می‌گردد. این به معنای آن است که می‌توانیم چندین عملیات غیرهم‌زمان را به صورت همزمان آغاز کنیم و سپس با استفاده از await از تکمیل آن‌ها مطلع شویم.
مدیریت خطا:
خطاهایی که در داخل عملیات‌های await رخ می‌دهد، به صورت خودکار به Task مربوطه منتقل شده و می‌توانند در بلوک‌های try-catch مدیریت شوند. این ویژگی، فرآیند اشکال‌زدایی و نگهداری کد را بسیار ساده می‌کند.

مثال عملی و کاربردی

در مثال زیر نحوه استفاده از کلیدواژه‌های async و await برای اجرای یک عملیات غیرهم‌زمان (شبیه‌سازی عملیات طولانی با تاخیر) نمایش داده شده است:

using System;
using System.Threading.Tasks;

class Program
{
    // متد اصلی برنامه که به صورت async تعریف شده است
    static async Task Main(string[] args)
    {
        Console.WriteLine("شروع عملیات...");
        
        // فراخوانی متد غیرهم‌زمان با استفاده از await
        await PerformAsyncOperation();
        
        Console.WriteLine("عملیات غیرهم‌زمان به پایان رسید.");
    }

    // متدی برای شبیه‌سازی یک عملیات طولانی
    static async Task PerformAsyncOperation()
    {
        // استفاده از await برای توقف اجرای کد تا تکمیل Task.Delay (تاخیر ۳ ثانیه‌ای)
        await Task.Delay(3000);
        Console.WriteLine("عملیات در داخل PerformAsyncOperation انجام شد.");
    }
}

توضیحات مثال:

متد Main به عنوان نقطه ورود برنامه به صورت async تعریف شده است تا بتواند از await استفاده کند.
در متد PerformAsyncOperation، با استفاده از await Task.Delay(3000) یک تاخیر ۳ ثانیه‌ای اعمال می‌شود. در این مدت، نخ اصلی آزاد است و می‌تواند به سایر وظایف پاسخ دهد.
پس از اتمام Task.Delay، اجرای متد از همان نقطه ادامه پیدا کرده و پیام مربوطه چاپ می‌شود.

مزایا و چالش‌های استفاده از async و await

مزایا:

ساده‌سازی کد:
با استفاده از async/await، ساختار کد به صورتی تبدیل می‌شود که بسیار شبیه کدهای هم‌زمان است و این امر باعث بهبود خوانایی و نگهداری کد می‌شود.

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

مدیریت آسان خطا:
با استفاده از بلوک‌های try-catch، می‌توان خطاهای رخ داده در متدهای async را به صورت یکپارچه مدیریت کرد.

چالش‌ها:

پیچیدگی مفهومی:
درک دقیق نحوه کارکرد await، به ویژه در مواردی که چندین عملیات غیرهم‌زمان به صورت تو در تو فراخوانی می‌شوند، ممکن است برای مبتدیان کمی دشوار باشد.

تغییر SynchronizationContext:
در برنامه‌های UI، استفاده از async/await نیازمند توجه به SynchronizationContext است تا پس از اتمام عملیات غیرهم‌زمان، کنترل به نخ صحیح (مانند نخ UI) بازگردد.

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

 نکات پیشرفته

ترکیب با CancellationToken:

در بسیاری از سناریوها، نیاز است که عملیات‌های غیرهم‌زمان قابل لغو باشند. با ترکیب async/await با CancellationToken می‌توان این نیاز را به راحتی برطرف کرد.

استفاده از ConfigureAwait:

در برخی موارد (به ویژه در کتابخانه‌ها یا کدهایی که نیازی به SynchronizationContext ندارند)، می‌توان با استفاده از متد ConfigureAwait(false) از انتقال خودکار به نخ اصلی جلوگیری کرد و کارایی را بهبود بخشید.

تست و عیب‌یابی:

استفاده از async/await موجب تغییر نحوه جریان اجرای کد می‌شود؛ بنابراین تست و عیب‌یابی دقیق در این بخش اهمیت ویژه‌ای دارد.

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

کار با Task و Parallel Programming

بررسی پیشرفته Task ها

 ساختار Task و نحوه ایجاد آن

ایجاد Task به صورت دستی:

علاوه بر استفاده از Task.Run و Task.Factory.StartNew، می‌توانید یک شیء Task را به صورت دستی ایجاد کنید و سپس آن را اجرا کنید. این امکان می‌تواند در مواقعی که نیاز به کنترل دقیق‌تر بر زمان‌بندی و اجرای Task دارید، مفید باشد.

Task myTask = new Task(() => {
    // عملیات مورد نظر
    Console.WriteLine("Task به صورت دستی ایجاد و اجرا شد.");
});
myTask.Start();

Task‌های والد و فرزند:

در سناریوهای پیچیده، ممکن است بخواهید چندین Task به صورت سلسله مراتبی اجرا شوند. با استفاده از ویژگی‌های Parent-Child Task می‌توانید Task‌های زیرمجموعه ایجاد کنید که وابستگی‌های منطقی میان آن‌ها برقرار است.
این امکان باعث می‌شود در صورت بروز خطا یا لغو عملیات در یک Task والد، Task‌های فرزند نیز به درستی مدیریت شوند.

 مدیریت استثنا در Task ها

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

try
{
    Task[] tasks = new Task[3];
    for (int i = 0; i < 3; i++)
    {
        tasks[i] = Task.Run(() => {
            // عملیات که ممکن است خطا ایجاد کند
            throw new Exception($"خطا در Task {i}");
        });
    }
    Task.WaitAll(tasks);
}
catch (AggregateException ex)
{
    foreach (var innerEx in ex.InnerExceptions)
    {
        Console.WriteLine(innerEx.Message);
    }
}

 لغو Task ها (Cancellation)

CancellationToken و CancellationTokenSource:
یکی از قابلیت‌های بسیار مهم در Task‌ها، امکان لغو عملیات‌های در حال اجرا است.
با استفاده از CancellationTokenSource یک توکن لغو تولید می‌شود و به Task ارسال می‌گردد. در داخل Task، می‌توان به کمک متد ThrowIfCancellationRequested یا بررسی مقدار توکن، از ادامه عملیات جلوگیری کرد.

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        
        Task longRunningTask = Task.Run(() => {
            for (int i = 0; i < 10; i++)
            {
                cts.Token.ThrowIfCancellationRequested();
                Console.WriteLine($"مرحله {i} در حال اجرا");
                Thread.Sleep(500);
            }
        }, cts.Token);

        // لغو Task پس از ۲ ثانیه
        await Task.Delay(2000);
        cts.Cancel();

        try
        {
            await longRunningTask;
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("عملیات لغو شد.");
        }
    }
}

زمان‌بندی و TaskScheduler

تنظیم زمان‌بندی Task:
دات‌نت یک شیء به نام TaskScheduler فراهم می‌کند که وظیفه زمان‌بندی اجرای Task ها را بر عهده دارد. به صورت پیش‌فرض، Task ها از Thread Pool استفاده می‌کنند، اما می‌توانید یک زمان‌بندی سفارشی تعریف کنید.
این امکان به شما کمک می‌کند تا کنترل بهتری بر نحوه تخصیص منابع سیستم داشته باشید.

بررسی پیشرفته Parallel Programming

 نحوه کار Parallel.For و Parallel.ForEach

Parallel.For:
این تابع یک حلقه for را به صورت موازی اجرا می‌کند. متد Parallel.For به صورت خودکار بار کاری را بین نخ‌های موجود در Thread Pool تقسیم می‌کند.
برای مثال، اگر یک حلقه for با تعداد تکرارهای بالا داشته باشید، استفاده از Parallel.For می‌تواند به طرز چشمگیری زمان اجرا را کاهش دهد.

Parallel.For(0, 100, i =>
{
    // هر تکرار به صورت موازی اجرا می‌شود
    Console.WriteLine($"تکرار {i} در نخ {Task.CurrentId}");
});

Parallel.ForEach:
مشابه با Parallel.For اما برای مجموعه‌های داده‌ای مانند لیست‌ها و آرایه‌ها استفاده می‌شود.
با استفاده از این تابع می‌توانید به سادگی بر روی عناصر یک مجموعه به صورت موازی پردازش انجام دهید.

int[] data = { 1, 2, 3, 4, 5 };
Parallel.ForEach(data, number =>
{
    Console.WriteLine($"عدد {number} در نخ {Task.CurrentId}");
});

 Parallel.Invoke

اجرای چندین عمل مستقل:

اگر نیاز دارید چندین متد یا اکشن مستقل را به صورت موازی اجرا کنید، از Parallel.Invoke استفاده کنید.
این متد تمام اکشن‌های داده شده را همزمان اجرا می‌کند و پس از پایان همه آن‌ها، کنترل به برنامه باز می‌گردد.

Parallel.Invoke(
    () => { Console.WriteLine("عملیات اول در حال اجرا است."); },
    () => { Console.WriteLine("عملیات دوم در حال اجرا است."); },
    () => { Console.WriteLine("عملیات سوم در حال اجرا است."); }
);

مدیریت همگام‌سازی در برنامه‌نویسی موازی

مسائل همگام‌سازی:

هنگام استفاده از Parallel Programming، اگر چندین نخ به یک منبع مشترک دسترسی پیدا کنند، احتمال بروز شرایط رقابتی (Race Conditions) وجود دارد.
برای جلوگیری از این مشکلات باید از سازوکارهای همگام‌سازی مانند lock, Monitor, یا استفاده از ساختارهای داده‌ای ایمن در برابر نخ (مانند ConcurrentBag، ConcurrentDictionary) استفاده کنید.

int counter = 0;
object lockObject = new object();

Parallel.For(0, 1000, i =>
{
    lock (lockObject)
    {
        counter++;
    }
});
Console.WriteLine($"مقدار نهایی counter: {counter}");

 PLINQ (Parallel LINQ)

معرفی PLINQ:

PLINQ (Parallel LINQ) یک گسترش برای LINQ است که به شما اجازه می‌دهد تا روی مجموعه‌های داده‌ای به صورت موازی عملیات فیلتر، مرتب‌سازی و پردازش انجام دهید.
استفاده از PLINQ می‌تواند کدهای LINQ شما را بسیار سریع‌تر کند، به ویژه در پردازش‌های داده‌ای بزرگ.

using System.Linq;

int[] numbers = Enumerable.Range(1, 10000).ToArray();
var evenNumbers = numbers.AsParallel().Where(n => n % 2 == 0).ToArray();
Console.WriteLine($"تعداد اعداد زوج: {evenNumbers.Length}");

 انتخاب و ترکیب Task ها و Parallel Programming

سناریوهای استفاده از Task

عملیات I/O-bound:
مانند درخواست‌های شبکه، خواندن و نوشتن فایل یا ارتباط با پایگاه داده. در این موارد، استفاده از Task به همراه async/await بهترین گزینه است زیرا نخ اصلی مسدود نمی‌شود.
مدیریت نتیجه و ترکیب چند عملیات:
هنگامی که نیاز دارید چند عملیات غیرهم‌زمان را اجرا کنید و سپس نتایج آن‌ها را ترکیب یا بررسی کنید، Task ها ابزار مناسبی هستند.

سناریوهای استفاده از Parallel Programming

عملیات CPU-bound:
برای محاسبات سنگین که نیاز به استفاده از تمام هسته‌های پردازنده دارند، استفاده از Parallel Programming (مانند Parallel.For یا Parallel.ForEach) بسیار مناسب است.
پردازش داده‌های بزرگ:
زمانی که باید مجموعه‌های بزرگی از داده‌ها را فیلتر یا پردازش کنید، PLINQ و متدهای موازی می‌توانند زمان اجرا را به طرز چشمگیری کاهش دهند.
اجرای اکشن‌های مستقل:
اگر چندین تابع یا اکشن مستقل دارید که به یکدیگر وابستگی ندارند، Parallel.Invoke یک راهکار سریع برای اجرای همزمان آن‌ها است.

نکات نهایی و بهینه‌سازی

استفاده از Profiling:

برای شناسایی گلوگاه‌های عملکردی در برنامه‌های هم‌زمان و موازی، از ابزارهای پروفایلینگ مانند Visual Studio Profiler استفاده کنید. این ابزارها به شما نشان می‌دهند که کدام بخش از کد زمان بیشتری مصرف می‌کند و بهینه‌سازی‌های لازم را پیشنهاد می‌دهند.

تست و اشکال‌زدایی:

اجرای کدهای موازی می‌تواند منجر به بروز شرایط رقابتی و مشکلات همگام‌سازی شود. تست‌های واحد (Unit Testing) و تست‌های همزمانی (Concurrency Testing) برای اطمینان از صحت عملکرد کدهای شما بسیار حیاتی هستند.

مدیریت حافظه و منابع:

در استفاده از Task ها و Parallel Programming، مطمئن شوید که منابع مانند فایل‌ها یا اتصالات شبکه به درستی آزاد می‌شوند. استفاده از الگوهایی مانند using یا فراخوانی متدهای Dispose می‌تواند از نشت منابع جلوگیری کند.

تنظیمات پیشرفته TaskScheduler:

در سناریوهایی که نیاز به کنترل دقیق‌تر بر روی اجرای Task ها دارید، می‌توانید یک TaskScheduler سفارشی پیاده‌سازی کرده و زمان‌بندی Task ها را بر اساس نیازهای خاص خود تنظیم کنید.

در نهایت:

کار با Task و Parallel Programming در سی شارپ دو رویکرد مکمل برای دستیابی به برنامه‌های سریع‌تر، پاسخگوتر و کارآمدتر هستند.

Task ها به شما این امکان را می‌دهند که با استفاده از الگوی async/await، عملیات‌های I/O-bound را به سادگی مدیریت کنید، نتیجه آن‌ها را به صورت غیرهم‌زمان دریافت کنید و از پیچیدگی‌های مدیریت مستقیم نخ‌ها اجتناب کنید.
Parallel Programming با ارائه توابعی مانند Parallel.For، Parallel.ForEach و Parallel.Invoke به شما اجازه می‌دهد تا عملیات‌های CPU-bound و پردازش‌های سنگین را به طور همزمان و با بهره‌گیری از چندین هسته پردازنده اجرا کنید.
با درک دقیق مفاهیم پیشرفته، استفاده صحیح از سازوکارهای لغو، مدیریت استثنا، همگام‌سازی داده‌ها و استفاده از ابزارهای بهینه‌سازی، می‌توانید کدهایی توسعه دهید که همزمان از نظر عملکرد بهینه و هم از نظر نگهداری و خوانایی قابل مدیریت باشند.

مدیریت وضعیت‌های غیرهم‌زمان در C#

بهبود مدیریت وضعیت در سناریوهای پیچیده

 پیگیری جریان‌های غیرهم‌زمان (Asynchronous Flows)

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

استفاده از AsyncLocal<T>:

این ابزار به شما اجازه می‌دهد که اطلاعات مخصوص هر جریان غیرهم‌زمان (مانند شناسه کاربر، تراکنش یا وضعیت جاری) را بدون تاثیر بر جریان‌های دیگر نگهداری کنید. این روش به ویژه در سرویس‌های وب و برنامه‌هایی که نیاز به ردگیری داده‌های کانتکست در سراسر Task‌ها دارند مفید است.

AsyncLocal<string> currentOperation = new AsyncLocal<string>();

async Task PerformOperationAsync()
{
    currentOperation.Value = "عملیات آغاز شده";
    Console.WriteLine($"وضعیت: {currentOperation.Value}");
    await Task.Delay(1000);
    currentOperation.Value = "عملیات در حال پردازش";
    Console.WriteLine($"وضعیت: {currentOperation.Value}");
    await Task.Delay(1000);
    currentOperation.Value = "عملیات به پایان رسید";
    Console.WriteLine($"وضعیت: {currentOperation.Value}");
}

استفاده از جریان‌های داده‌ای (Dataflow):

با استفاده از کتابخانه TPL Dataflow می‌توانید جریان‌های پیچیده داده‌ای را به صورت همزمان مدیریت کنید. این کتابخانه به شما کمک می‌کند تا بلوک‌های پردازشی مختلف را به هم متصل کرده و جریان‌های داده‌ای را به صورت امن و همزمان مدیریت کنید.

 ثبت و مانیتورینگ وضعیت‌ها

یکی از موارد مهم در مدیریت وضعیت‌های غیرهم‌زمان، ثبت و نظارت بر رویدادهای رخ داده در طول اجرای Task‌ها و عملیات‌های غیرهم‌زمان است.

Logging جامع:

ثبت رویدادهای مهم مانند شروع و پایان یک Task، بروز خطا، لغو عملیات یا تغییر وضعیت می‌تواند در عیب‌یابی و بهبود عملکرد برنامه بسیار کمک‌کننده باشد. استفاده از کتابخانه‌هایی مانند NLog، log4net یا Serilog به شما این امکان را می‌دهد که اطلاعات دقیق و ساختاریافته‌ای از جریان‌های غیرهم‌زمان جمع‌آوری کنید.

Tracing و Diagnostics:

ابزارهای پیشرفته نظیر Application Insights یا امکانات built-in در Visual Studio (مانند Diagnostic Tools) به شما اجازه می‌دهند که جریان‌های غیرهم‌زمان، زمان‌بندی Task‌ها و نقاط بحرانی در عملکرد برنامه را شناسایی و آنالیز کنید.

 بهبود همگام‌سازی داده‌های مشترک

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

هنگام کار با داده‌های مشترک در برنامه‌های غیرهم‌زمان، باید به دقت مدیریت دسترسی‌ها را انجام دهید تا از بروز مشکلاتی مانند Race Condition، Deadlock یا Starvation جلوگیری شود.

Lock و Monitor:

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

private readonly object _syncLock = new object();
private int _sharedResource = 0;

async Task UpdateResourceAsync()
{
    await Task.Run(() =>
    {
        lock (_syncLock)
        {
            // به روزرسانی امن متغیر مشترک
            _sharedResource++;
            Console.WriteLine($"مقدار به‌روز شده: {_sharedResource}");
        }
    });
}

ساختارهای داده‌ای همزمان:

کلاس‌هایی مانند ConcurrentDictionary، ConcurrentQueue و ConcurrentBag به صورت داخلی از قفل‌های بهینه و مکانیزم‌های هماهنگ‌سازی استفاده می‌کنند که مدیریت دسترسی به داده‌های مشترک را ساده و امن می‌سازند.

استفاده از Interlocked:

برای عملیات‌های ساده‌ای مانند افزایش یا کاهش یک متغیر عددی، استفاده از متدهای Interlocked یک روش اتمیک و بدون نیاز به قفل اضافی است.

int count = 0;
Interlocked.Increment(ref count);
Console.WriteLine($"مقدار شمارنده: {count}");

 مدیریت خطاها در سناریوهای پیچیده غیرهم‌زمان

 استراتژی‌های چندلایه در مدیریت خطا

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

خطاهای سطح Task:

در هر Task، خطاها با استفاده از بلوک‌های try-catch مدیریت می‌شوند. اما در سناریوهای موازی، لازم است خطاها را از طریق AggregateException جمع‌آوری و تحلیل کنید.

خطاهای سطح سیستم:

برای خطاهایی که خارج از Task‌ها رخ می‌دهند (مانند خطاهای شبکه، خطاهای پایگاه داده یا خطاهای سیستم‌عامل)، باید مکانیزم‌های ثبت و گزارش‌دهی مناسبی داشته باشید تا بتوانید به سرعت به آن‌ها واکنش نشان دهید.

استفاده از الگوهای طراحی برای مدیریت خطا

Circuit Breaker Pattern:

این الگو برای جلوگیری از تلاش‌های مکرر به سمت یک سرویس یا بخش آسیب‌پذیر استفاده می‌شود. در صورتی که خطاهای متوالی رخ دهند، سیستم به حالت «Breaker» می‌رود تا به بخش آسیب‌پذیر فرصت بازیابی داده شود.

Retry Pattern:

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

async Task<T> RetryAsync<T>(Func<Task<T>> operation, int maxAttempts = 3, int delayMilliseconds = 500)
{
    for (int attempt = 0; attempt < maxAttempts; attempt++)
    {
        try
        {
            return await operation();
        }
        catch (Exception ex) when (attempt < maxAttempts - 1)
        {
            Console.WriteLine($"تلاش {attempt + 1} شکست خورد. در حال تکرار...");
            await Task.Delay(delayMilliseconds);
        }
    }
    throw new Exception("تمام تلاش‌ها ناموفق بود.");
}

هماهنگی بین عملیات‌های غیرهم‌زمان و رابط کاربری

SynchronizationContext و به‌روزرسانی UI

در برنامه‌های دارای رابط کاربری مانند WinForms یا WPF، مهم است که عملیات‌های غیرهم‌زمان پس از اتمام، کنترل به نخ UI بازگردانند تا تغییرات در UI به درستی اعمال شود. استفاده از SynchronizationContext در این موارد بسیار حیاتی است.

ConfigureAwait:

استفاده از ConfigureAwait در متدهای غیرهم‌زمان به شما این امکان را می‌دهد که کنترل بازگشتی به نخ فعلی (مانند نخ UI) یا به نخ دیگری منتقل شود.

// اجرای یک عملیات غیرهم‌زمان بدون نیاز به بازگشت به SynchronizationContext فعلی
await Task.Delay(1000).ConfigureAwait(false);

 

الگوهای MVVM:

در برنامه‌های WPF، استفاده از الگوی MVVM به تفکیک منطق تجاری از منطق UI کمک می‌کند و به مدیریت بهتر جریان‌های غیرهم‌زمان و به‌روزرسانی UI منجر می‌شود.

بهترین شیوه‌ها و نکات تکمیلی

تست و اشکال‌زدایی:

به دلیل پیچیدگی‌های موجود در برنامه‌های غیرهم‌زمان، نوشتن تست‌های واحد برای سناریوهای مختلف و استفاده از ابزارهای پیشرفته اشکال‌زدایی (مانند Time Travel Debugging) توصیه می‌شود.

مستندسازی جریان‌های غیرهم‌زمان:

به‌ویژه در پروژه‌های بزرگ، مستندسازی دقیق جریان‌های غیرهم‌زمان و الگوهای مدیریت خطا و وضعیت به تیم توسعه کمک می‌کند تا درک بهتری از ساختار سیستم داشته باشند.

بهینه‌سازی منابع:

همیشه توجه داشته باشید که منابع مصرفی مانند اتصالات شبکه، فایل‌ها یا اشیاء IDisposable را پس از پایان استفاده آزاد کنید. استفاده از بلوک‌های using و فراخوانی Dispose به صورت منظم از نشت منابع جلوگیری می‌کند.

پیگیری و Logging:

یک سیستم logging دقیق برای ثبت وقایع غیرهم‌زمان می‌تواند در تشخیص و رفع مشکلات به شما کمک کند. ابزارهای logging مدرن به شما این امکان را می‌دهند که جریان اجرای Taskها، استثناها و وضعیت‌های مختلف را به صورت لحظه‌ای نظارت کنید.

به طور خلاصه:

مدیریت وضعیت‌های غیرهم‌زمان در C# یک چالش پیچیده است که نیازمند به کارگیری استراتژی‌های مختلف برای مدیریت خطاها، لغو عملیات، همگام‌سازی داده‌های مشترک و هماهنگی با رابط کاربری می‌باشد. با پیاده‌سازی راهکارهای مطرح شده در این مطلب می‌توانید:

از طریق استفاده هوشمندانه از try-catch و AggregateException، خطاهای رخ داده را به طور موثری مدیریت کنید.
با بهره‌گیری از CancellationToken و پیاده‌سازی استراتژی‌های لغو، کنترل دقیقی بر روی عملیات‌های طولانی داشته باشید.
با استفاده از ابزارهایی مانند AsyncLocal و سازوکارهای همگام‌سازی (lock، Interlocked، ساختارهای داده‌ای همزمان) از تداخل‌های دسترسی به داده‌های مشترک جلوگیری کنید.
با هماهنگی صحیح میان اجرای غیرهم‌زمان و به‌روزرسانی UI (استفاده از SynchronizationContext، ConfigureAwait، و الگوهای MVVM) تجربه کاربری بهتری ارائه دهید.
از الگوهای طراحی مانند Retry و Circuit Breaker برای بهبود قابلیت اطمینان و کاهش اثر خطاهای موقتی استفاده کنید.
با در نظر گرفتن این نکات و به کارگیری بهترین شیوه‌های مدیریت وضعیت، می‌توانید برنامه‌هایی بسازید که در مواجهه با پیچیدگی‌های محیط‌های هم‌زمان و غیرهم‌زمان، پایدار، پاسخگو و مقاوم در برابر خطا باشند.

نتیجه‌گیری

در این مقاله به بررسی جامع مفاهیم و تکنیک‌های مرتبط با برنامه‌نویسی هم‌زمان و غیرهم‌زمان در سی شارپ پرداختیم. از مباحث پایه‌ای مانند تعریف و تفاوت برنامه‌نویسی هم‌زمان و غیرهم‌زمان گرفته تا آشنایی با کلیدواژه‌های async و await، کار با Task و Parallel Programming و مدیریت وضعیت‌های غیرهم‌زمان، همه جنبه‌های مهم این حوزه پوشش داده شد. استفاده از این تکنیک‌ها باعث می‌شود که برنامه‌ها بتوانند به صورت هم‌زمان عملیات‌های متعدد را اجرا کرده و در عین حال به درخواست‌های کاربر پاسخگو باقی بمانند. همچنین، به کمک ابزارهایی مانند CancellationToken، SynchronizationContext و ساختارهای داده‌ای همزمان، می‌توان خطاها و مشکلات مرتبط با همگام‌سازی داده‌ها را به شکل بهینه مدیریت کرد.

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

آموزش برنامه‌نویسی هم‌زمان و غیرهم‌زمان در سی شارپ

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

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

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