نوشته‌ها

استفاده از متد WhenAll برای اجرای چندین Task به صورت همزمان در سی شارپ

فرض کنید که داخل یک متد باید چندین متد را به صورت await فراخوانی کنید. به صورت عادی زمانی که متدها فراخوانی می شوند هر بخش await بعد از تکمیل await قبلی اجرا خواهد شد و مقادیر بازگشتی به صورت یکجا در اختیار شما قرار نمیگیرند. برای مثال، کد زیر را در نظر بگیرید:

private async void AsyncBtn_Click(object sender, EventArgs e)
{
    Result1TextBox.Text = (await Task1()).ToString();
    Result12extBox.Text = (await Task2()).ToString();
}
 
private Task < long > Task1()
{
    return Task.Run<long>(() = >
    {
        var num = Enumerable.Repeat(10, 1000);
        long sum = 0;
        foreach (var item in num)
        {
            System.Threading.Thread.Sleep(2);
            sum += item;
        }
        return sum;
    });
}
private Task < long > Task2()
{
    return Task.Run<long>(() = >
    {
        var num = Enumerable.Repeat(10, 1000);
        long sum = 0;
        foreach (var item in num)
        {
            System.Threading.Thread.Sleep(2);
            sum += item;
        }
        return sum;
    });
}

 

در کد بالا، ابتدا عملیات Task1 انجام شده و نتیجه نمایش داده می شود و پس از آن Task2 اجرا شده و نتیجه نمایش داده می شود. برای رفع وقفه بین اجرای دو Task از متد WhenAll استفاده می کنیم. برای استفاده از متد WhenAll کد BtnAsync_Click را به صورت زیر تغییر می دهیم:

 

private async void AsyncBtn_Click(object sender, EventArgs e)
{
    var results = await Task.WhenAll(Task1(), Task2());
    txtBox.Text = results[0].ToString();
    txtSecond.Text = results[1].ToString();
}

 

با ایجاد تغییر کد بالا، خروجی متد WhenAll یک آرایه از نوع long خواهد بود که هر یک از اندیس های آرایه به ترتیب خروجی متدهای اول و دوم می باشد و به صورت بالا می توان خروجی ها را در TextBox ها نمایش داد.

منبع


قسمت اول آموزش-برنامه نویسی Asynchronous – آشنایی با Process ها، Thread ها و AppDomain ها

قسمت دوم آموزش- آشنایی با ماهیت Asynchronous در Delegate ها

قسمت سوم آموزش-آشنایی با فضای نام System.Threading و کلاس Thread

قسمت چهارم آموزش- آشنایی با Thread های Foreground و Background در دات نت

قسمت پنجم آموزش- آشنایی با مشکل Concurrency در برنامه های Multi-Threaded و راهکار های رفع این مشکل

قسمت ششم آموزش- آشنایی با کلاس Timer در زبان سی شارپ

قسمت هفتم آموزش-آشنایی با CLR ThreadPool در دات نت

قسمت هشتم آموزش- مقدمه ای بر Task Parallel Library و کلاس Parallel در دات نت

قسمت نهم آموزش- برنامه نویسی Parallel:آشنایی با کلاس Task در سی شارپ

قسمت دهم آموزش-برنامه نویسی Parallel در سی شارپ :: متوقف کردن Task ها در سی شارپ – کلاس CancellationToken

قسمت یازدهم آموزش- برنامه نویسی Parallel در سی شارپ :: کوئری های Parallel در LINQ

قسمت دوازدهم آموزش- آشنایی با کلمات کلیدی async و await در زبان سی شارپ

قسمت سیزدهم آموزش- استفاده از متد WhenAll برای اجرای چندین Task به صورت همزمان در سی شارپ

 

 

 

برنامه نویسی Parallel در سی شارپ :: متوقف کردن Task ها در سی شارپ – کلاس CancellationToken

زمانی که عملیاتی را به عنوان یک Task اجرا می کنیم، ممکن است بخواهیم آن Task را در حین اجرا متوقف کنیم، برای مثال، Task ای داریم که در حال پردازش ۱۰۰۰ فایل است و کاربر باید این امکان را داشته باشد که Task در حال اجرا را متوقف کند. عملیات متوقف کردن Task ها هم برای متدهای کلاس Parallel امکان پذیر است و هم کلاس Task. برای اینکار می بایست از کلاس CancellationToken استفاده کنیم. برای مثال Task زیر را در نظر بگیرید که حاصل میانگین جمع اعداد ۱ تا ۱۰۰ را محاسبه می کند:

Task < int > averageTask = new Task < int > (() =>
{
    Console.WriteLine("Calculating average...");
    Console.WriteLine("Press Ctrl+C to cancel...");
    var sum = 0;
    for (int counter = 1; counter < = 100; counter++)
    {
        sum += counter;
        Thread.Sleep(100);
    }
    Console.WriteLine("All done.");
    return sum/100;
});
averageTask.Start();
Console.WriteLine(averageTask.Result);

قبلاً با این کد آشنا شدیم، اما کاری که در این قسمت می خواهیم انجام دهیم اضافه کردن قابلیتی است که کاربر بتواند با فشردن کلید های Ctrl+C عملیات را متوقف کند. برای اینکار ابتدا شئ ای از نوع کلاس CancellationTokenSource که در فضای نام System.Threading قرار دارد، در کلاس Program به صورت زیر تعریف می کنیم:

Task < int > averageTask = new Task < int > (() = >
{
    Console.WriteLine("Calculating average...");
    Console.WriteLine("Press q to cancel...");
    var sum = 0;
    for (int counter = 1; counter < = 100; counter++)
    {
        sum += counter;
        Thread.Sleep(100);
    }
    Console.WriteLine("All done.");
    return sum/100;
}, source.Token);

شئ source که در کلاس Program ایجاد کردیم متدی دارد با نام Cancel که این متد را زمانی که قصد داریم Task متوقف شود باید فراخوانی کنیم. فراخوانی این متد باید زمانی انجام شود که کاربر کلید های Ctrl+C را فشار داده است. در محیط Console، زمانی که کاربر کلید های Ctrl+C را فشار می دهد، event ای با نام CancelPressKey در کلاس Console فراخوانی می شود، پس باید این از این event برای فراخوانی متد Cancel به صورت زیر استفاده کنیم:

Console.CancelKeyPress += (sender, eventArgs) = >
{
    source.Cancel();
    eventArgs.Cancel = true;
};

به خط دوم داخل event دقت کنید، زمانی که کلید های Ctrl+C فشرده می شوند، به صورت پیش فرض کل برنامه Console متوقف می شود، برای جلوگیری از این کار مقدار خصوصیت Cancel را در شئ eventArgs به مقدار true ست می کنیم، یعنی عملیات متوقف کردن محیط کنسول به صورت دستی توسط ما انجام شده و خود سیستم نیاز به انجام کاری در این باره ندارد.

بعد از Subscribe کردن event بالا، باید به برنامه بگوییم تا زمانی که task به اتمام نرسیده یا کاربر کلید های Ctrl+C را فشار نداده نباید از برنامه خارج شویم، به همین خاطر یک حلقه while به صورت زیر ایجاد می کنیم:

while (!averageTask.IsCompleted &amp;&amp; !source.IsCancellationRequested)
{                                                                                                
}

با خصوصیت IsCompleted در کلاس Task قبلاً آشنا شدیم، اما خصوصیت IsCancellationRequested در شئ source زمانی مقدارش true می شود که متد Cancel فراخوانی شود، پس تا زمانی که عملیات Task به اتمام نرسیده و زمانی که کاربر کلید های Ctrl+C را فشار نداده برنامه در حلقه while منتظر می ماند.

در ادامه باید Task ایجاد شده را به صورتی تغییر دهیم که داخل حلقه for بررسی شود که متد Cancel فراخوانی شده است یا خیر، اگر فراخوانی شده بود باید از Task خارج شویم، برای این کار نیز از خصوصیت IsCancellationRequested در شئ source استفاده می کنیم، Task ایجاد شده را به صورت زیر تغییر می دهیم:

Task < int > averageTask = new Task < int > (() = >
{
    Console.WriteLine("Calculating average...");
    Console.WriteLine("Press Ctrl+C to cancel...");
    var sum = 0;
    for (int counter = 1; counter < = 100; counter++)
    {
        if (source.IsCancellationRequested)
        {
            Console.WriteLine("Operation terminated!");
            return 0;
        }
        sum += counter;
        Thread.Sleep(100);
    }
    Console.WriteLine("All done.");
    return sum/100;
}, source.Token);

همانطور که مشاهده می کنید داخل حلقه for گفتیم که اگر IsCancellationRequested برابر true بود پیغامی را نمایش بده و مقدار ۰ را برگردان. کد نهایی ما به صورت زیر می باشد:

class Program
{
    private static CancellationTokenSource source = new CancellationTokenSource();
    static void Main(string[] args)
    {
        Task < int > averageTask = new Task < int >(() = >
        {
            Console.WriteLine("Calculating average...");
            Console.WriteLine("Press Ctrl+C to cancel...");
            var sum = 0;
            for (int counter = 1; counter <= 100; counter++) { if (source.IsCancellationRequested) { Console.WriteLine("Operation terminated!"); return 0; } sum += counter; Thread.Sleep(100); } Console.WriteLine("All done."); return sum/100; }, source.Token); averageTask.Start(); Console.CancelKeyPress += (sender, eventArgs) = >
        {
            source.Cancel();
            eventArgs.Cancel = true;
        };
        while (!averageTask.IsCompleted && !source.IsCancellationRequested)
        {                                                                                                
        }
 
        Console.WriteLine(averageTask.Result);
    }
}

در صورتی که برنامه بالا را اجرا کرده و کلید های Ctrl+C را فشار دهیم خروجی زیر برای ما نمایش داده می شود:

Calculating average...
Press Ctrl+C to cancel...
Operation terminated!
۰
Press any key to continue . . .

استفاده از CancellationToken در کلاس Parallel

علاوه بر کلاس Task می توان از قابلیت CancellationToken در متدهای کلاس Parallel نیز استفاده کرد، برای آشنایی بیشتر فرض کنید کدی به صورت زیر تعریف شده که لیست فایل های jpg داخل یک پوشه را پردازش می کند:

var jpegFiles = System.IO.Directory.GetFiles("D:\\Images", "*.jpg");
 
Parallel.ForEach(jpegFiles, file = >
{
    var fileInfo = new FileInfo(file);
    // process file
});

برای متوقف کردن عملیات پردازش فایل ها، ابتدا شئ ای از نوع CancellationTokenSource مانند مثال قبل ایجاد می کنیم:

private static CancellationTokenSource source = new CancellationTokenSource();

در قدم بعدی کلاسی از نوع ParallelOptions به صورت زیر تعریف کرده، خصوصیت CancellationToken را برابر خصوصیت Token در شئ source قرار داده و این کلاس را به عنوان پارامتر ورودی به متد ForEach به صورت زیر ارسال می کنیم:

ParallelOptions options = new ParallelOptions();
options.CancellationToken = source.Token;
 
try
{
    Parallel.ForEach(jpegFiles,options, file = >
    {
        options.CancellationToken.ThrowIfCancellationRequested();                                                
        var fileInfo = new FileInfo(file);
        // process file
    });
}
catch (OperationCanceledException ex)
{
    Console.WriteLine(ex);
}

دقت کنید در قسمت ForEach متدی با نام ThrowIfCancellationRequested فراخوانی شده است، در حقیقت این متد بعد از فراخوانی بررسی می کند که آیا متد Cancel برای شئ source فراخوانی شده است یا خیر، اگر فراخوانی شده بود خطایی از نوع OperationCanceledException ایجاد می شود که در خارج از بدنه ForEach کلاس Parallel، بوسیله ساختار try..catch این خطا مدیریت شده است. دقت کنید که روند مدیریت Cancel کردن در کلاس Parallel با کلاس Task متفاوت است و دلیل این موضوع نوع برخورد برنامه با این کلاس ها است. در قسمت بعدی با مبحث Parallel LINQ آشنا خواهیم شد.

منبع



قسمت اول آموزش-برنامه نویسی Asynchronous – آشنایی با Process ها، Thread ها و AppDomain ها

قسمت دوم آموزش- آشنایی با ماهیت Asynchronous در Delegate ها

قسمت سوم آموزش-آشنایی با فضای نام System.Threading و کلاس Thread

قسمت چهارم آموزش- آشنایی با Thread های Foreground و Background در دات نت

قسمت پنجم آموزش- آشنایی با مشکل Concurrency در برنامه های Multi-Threaded و راهکار های رفع این مشکل

قسمت ششم آموزش- آشنایی با کلاس Timer در زبان سی شارپ

قسمت هفتم آموزش-آشنایی با CLR ThreadPool در دات نت

قسمت هشتم آموزش- مقدمه ای بر Task Parallel Library و کلاس Parallel در دات نت

قسمت نهم آموزش- برنامه نویسی Parallel:آشنایی با کلاس Task در سی شارپ

قسمت دهم آموزش-برنامه نویسی Parallel در سی شارپ :: متوقف کردن Task ها در سی شارپ – کلاس CancellationToken

قسمت یازدهم آموزش- برنامه نویسی Parallel در سی شارپ :: کوئری های Parallel در LINQ

قسمت دوازدهم آموزش- آشنایی با کلمات کلیدی async و await در زبان سی شارپ

قسمت سیزدهم آموزش- استفاده از متد WhenAll برای اجرای چندین Task به صورت همزمان در سی شارپ

آشنایی با کلمات کلیدی async و await در زبان سی شارپ

تا این لحظه از مجموعه مطالب مرتبط با مباحث Asynchronous Programming در سی شارپ با ماهیت Asynchronous در delegate ها، کار با Thread ها و کتابخانه TPL در دات نت آشنا شدیم. اما باز هم در برخی سناریو ها و انجام کارهای پیچیده در برنامه نویسی Asynchronous، نیاز به حجم زیادی از کدها دارد.

از نسخه ۴٫۵ دات، در زبان سی شارپ (و همینطور زبان VB) دو کلمه کلیدی اضافه شد که اجازه نوشتن کدهای Asynchronous را به شکل دیگری به برنامه نویسان می داد. این دو کلمه کلیدی، کلمات async و await هستند و زمانی که شما در کدهای خود از این دو کلمه کلیدی استفاده می کنید، در زمان کامپایل کدها، کامپایلر کدهایی را برای شما تولید می کند که به صورت بهینه و البته مطمئن کارهای Asynchronous را برای شما انجام می دهند، کدهای تولید شده از کلاس هایی که در فضای نام System.Threading.Tasks قرار دارند استفاده می کنند.

نگاه اولیه با ساختار async و await

زمانی که شما در بخشی از کد خود از کلمه کلیدی async و بر روی متدها، عبارات لامبدا یا متدهای بدون نام استفاده می کنید، در حقیقت می گویید که این قطعه کد به صورت خودکار باید به صورت Asynchronous فراخوانی شود و زمان استفاده از کدی که به صورت async تعریف شده، CLR به صورت خودکار thread جدیدی ایجاد کرده و کد را اجرا می کند. اما زمان فراخوانی کدهایی که به صورت async تعریف شده اند، استفاده از کلمه await این امکان را فراهم می کند که اجرای thread جاری تا زمان تکمیل اجرای کدی که به صورت async تعریف شده، می بایست متوقف شود.

برای آشنایی بیشتر برنامه ای از نوع Windows Forms Application ایجاد کرده، یک Button بر روی فرم قرار می دهیم. زمانی که بر روی Button ایجاد شده کلیک می شود، یک متد دیگر فراخوانی شده و بعد از یک وقفه ۱۰ ثانیه ای عبارتی را بر میگرداند و در نهایت این متن به عنوان Title برای فرم برنامه ست می شود:

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
    }
 
    private void CallButton_Click(object sender, EventArgs e)
    {
        this.Text = DoWork();
    }
 
    private string DoWork()
    {
        Thread.Sleep(10000);
        return "Done.";
    }
}

مشکلی که وجود دارد این است که بعد از کلیک بر روی Button ایجاد شده، ۱۰ ثانیه باید منتظر شده تا عنوان فرم تغییر کند. اما با انجام یکسری تغییرات در کد بالا، می توان بوسیله کلمات کلیدی async و await کاری کرد که عملیات اجرای متد به صورت Asynchronous انجام شود. برای اینکار کد بالا را به صورت زیر تغییر می دهیم:

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
    }
 
    private async void CallButton_Click(object sender, EventArgs e)
    {
        this.Text = await DoWork();
    }
 
    private Task<string> DoWork()
    {
        return Task.Run(() = >
        {
            Thread.Sleep(10000);
            return "Done.";
        });
    }
}

بعد از اجرای برنامه، خواهیم دید که فرم ما به قول معروف block نمی شود، یعنی تا زمان اتمام فراخوانی DoWork می توانیم کارهای دیگری در فرم انجام دهیم. اگر در کد بالا دقت کنید، متدی که برای رویداد Click دکمه CallButton تعریف شده، با کلمه کلیدی async مشخص شده، یعنی اجرای این متد باید به صورت Aynchronous انجام شود.

علاوه بر این، داخل بدنه این متد، زمان فراخوانی DoWork از کلمه await استفاده کردیم، دقت کنید که نوشتن کلمه کلیدی await اینجا الزامی است، اگر این کلمه کلیدی نوشته نشود، زمان اجرای DoWork باز هم عملیات فراخوانی متد باعث block شدن فرم ما می شود. همچنین دقت کنید که متد DoWork به جای اینکه مقدار string برگرداند، مقداری از نوع <Task<string بر میگرداند. به طور خلاصه کاری که DoWork انجام می دهد به صورت زیر است:

زمانی که متد DoWork فراخوانی می شود، یک Task جدید اجرا می شود و داخل Task ابتدا عملیات اجرای Thread به مدت ۱۰ ثانیه متوقف می شود و بعد از ۱۰ ثانیه یک رشته به عنوان خروجی برگردانده می شود. البته این رشته تحت یک شئ از نوع Task به متدی که DoWork را فراخوانی کرده بازگردانده می شود.

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

قواعد نام گذاری برای متدهای Async

همانطور که گفتیم، داخل متدهایی که با async مشخص شده اند، حتماً می بایست کلمه کلیدی await نیز نوشته شود. اما از کجا بدانیم کدام متدها می توانند به صورت Async فراخوانی شوند؟ یعنی نوع خروجی آن ها یک Task است؟ اصطلاحاً به متدهایی که خروجی آن ها از نوع <Task<T است Awaitable گفته می شود. برای اینکار باید از قواعد نامگذاری متدهای Async پیروی کنیم. بر اساس مستندات مایکروسافت، می بایست کلیه متدهایی که مقدار خروجی آن ها از نوع Task است، به صورت async تعریف شوند و در انتهای نام متد کلمه Async نوشته شود، بر اساس مطالب گفته شده، متد DoWork را به صورت زیر تغییر می دهیم:

private async Task<string> DoWorkAsync()
{
    return await Task.Run(() = >
    {
        Thread.Sleep(10000);
        return "Done.";
    });
}

با انجام تغییرات بالا، کد رویداد Click را برای CallButton به صورت زیر تغییر می دهیم:

private async void CallButton_Click(object sender, EventArgs e)
{
    this.Text = await DoWorkAsync();
}

متدهای Async با مقدار خروجی void

در صورتی که متدی که قرار است به صورت async فراخوانی شود، مقدار خروجی ندارد می توان نوع خروجی متد را از نوع کلاس غیر جنریک Task انتخاب کرد و کلمه کلیدی return را ننوشت:

private async Task DoWorkAsync()
{
    await Task.Run(() = >
    {
        Thread.Sleep(10000);
    });
}

فراخوانی این متد نیز به صورت زیر خواهد بود:

await DoWorkAsync();
MessageBox.Show("Done.");

متدهای async با چندین await

یکی از قابلیت های async و await، نوشتن چندین قسمت await در یک متد async است. نمونه کد زیر حالت گفته شده را نشان می دهد:

private async void CallButton_Click(object sender, EventArgs e)
{
    await Task.Run(() = > { Thread.Sleep(5000); });
    MessageBox.Show("First Task Done!");
 
    await Task.Run(() = > { Thread.Sleep(5000); });
    MessageBox.Show("Second Task Done!");
 
    await Task.Run(() = > { Thread.Sleep(5000); });
    MessageBox.Show("Third Task Done!");
}

دقت کنید که برای await های بالا متدی تعریف نکردیم و تنها در مقابل آن متد Run از کلاس Task را فراخوانی کردیم. البته این موضوع ربطی به چند await بودن متد ندارد و شما می تواند متد هایی که خروجی آن ها از نوع Task است را نیز فراخوانی کنید، این حالت تنها برای مثال به این صورت نوشته شده است.

منبع


قسمت اول آموزش-برنامه نویسی Asynchronous – آشنایی با Process ها، Thread ها و AppDomain ها

قسمت دوم آموزش- آشنایی با ماهیت Asynchronous در Delegate ها

قسمت سوم آموزش-آشنایی با فضای نام System.Threading و کلاس Thread

قسمت چهارم آموزش- آشنایی با Thread های Foreground و Background در دات نت

قسمت پنجم آموزش- آشنایی با مشکل Concurrency در برنامه های Multi-Threaded و راهکار های رفع این مشکل

قسمت ششم آموزش- آشنایی با کلاس Timer در زبان سی شارپ

قسمت هفتم آموزش-آشنایی با CLR ThreadPool در دات نت

قسمت هشتم آموزش- مقدمه ای بر Task Parallel Library و کلاس Parallel در دات نت

قسمت نهم آموزش- برنامه نویسی Parallel:آشنایی با کلاس Task در سی شارپ

قسمت دهم آموزش-برنامه نویسی Parallel در سی شارپ :: متوقف کردن Task ها در سی شارپ – کلاس CancellationToken

قسمت یازدهم آموزش- برنامه نویسی Parallel در سی شارپ :: کوئری های Parallel در LINQ

قسمت دوازدهم آموزش- آشنایی با کلمات کلیدی async و await در زبان سی شارپ

قسمت سیزدهم آموزش- استفاده از متد WhenAll برای اجرای چندین Task به صورت همزمان در سی شارپ

 

برنامه نویسی Parallel در سی شارپ :: مقدمه ای بر Task Parallel Library و کلاس Parallel در دات نت

پیش از این ما در سری مطالب مرتبط با بحث کار با Thread با نحوه ایجاد و مدیریت Thread ها در دات نت آشنا شدیم. از نسخه ۴ دات نت قابلیتی اضافه شد با نام Task Parallel Programming یا TPL که روش جدیدی برای نوشتن برنامه Multi-Theaded است. این قابلیت بوسیله یکسری از کلاس ها که در فضای نام System.Threading.Tasks قرار دارد فراهم شده و به ما این اجازه را می دهد که بدون درگیر شدن مستقیم با Thread ها و Thread Pool ها برنامه های Multi-Threaded بنوسیم.

دقت کنید که زمان استفاده از قابلیت TPL دیگر نیازی به استفاده از کلاس های فضای نام System.Threading نمی باشد و به صورت پشت زمینه عملیات ساخت و مدیریت Thread ها برای ما انجام می شود. با این کار شیوه کار با Threadها بسیار ساده شده و یکسری از پیچیدگی ها در این بین حذف می شود.

فضای نام System.Threading.Tasks

همانطور که گفتیم TPL در حقیقت مجموعه ای از کلاس ها است که در فضای نام System.Threading.Tasks قرار گرفته. یکی از قابلیت های TPL این است که کارهای محوله را به صورت خودکار بین CPU های سیستم (در صورت وجود) توزیع می کند که این کار در پشت زمینه بوسیله CLR Thread Pool انجام می شود.

کارهای انجام شده توسط TPL در پشت زمینه عبارتند از تقسیم بندی وظایف، زمانبندی Thread ها، مدیریت وضعیت (State Management) و یکسری از کارهای اصطلاحاً Low-Level دیگر. نتیجه این کار برای شما بالا رفتن کارآیی برنامه ها بوده بدون اینکه درگیر پیچیدگی های کار با Thread ها شوید. همانطور که گفتیم فضای نام System.Threading.Tasks شامل یکسری کلاس ها مانند کلاس Parallel، کلاس Task و … می باشد که در ادامه با این کلاس ها بیشتر آشنا می شویم.

نقش کلاس Parallel

یکی از کلاس های TPL که نقش کلیدی را در نوشتن کدهای Parallel ایفا می کند، کلاس Parallel است، این کلاس یکسری متدها در اختیار ما قرار می دهد که بتوانیم بر روی آیتم های یک مجموعه (علی الخصوص مجموعه هایی که اینترفیس IEnumerable را پیاده سازی کرده اند) به صورت parallel عملیات هایی را انجام دهیم.

متدهای این کلاس عبارتند از متد های For و ForEach که البته Overload های متفاوتی برای این متدها وجود دارد. بوسیله این متدها می توان کدهایی نوشتن که عملیات مورد نظر را به صورت parallel بر روی آیتم های یک مجموعه انجام دهند. دقت کنید کدهایی که برای این متدها نوشته می شوند در حقیقت همان کدهایی هستند که معمولاً در حلقه های for و foreach استفاده می شوند، با این تفاوت که به صورت parallel اجرا شده و اجرا و مدیریت کدها بوسیله thread ها و CLR Thread Pool انجام شده و البته بحث همزمانی نیز به صورت خودکار مدیریت می شود.

کار با متد ForEach

در ابتدا به سراغ متد ForEach می رویم، این متد یک مجموعه که ایترفیس IEnumerable را پیاده سازی کرده به عنوان پارامتر اول و متدی که باید بر روی هر یک اعضای این مجموعه انجام شود را به عنوان پارامتر دوم قبول می کند:

var numbers = new List &lt; int &gt; {2, 6, 8, 1, 3, 9, 6, 10, 5, 4};
Parallel.For(3, 6, index  = &gt;
{
    Console.WriteLine(numbers[index]);
    Console.WriteLine("Thread Id: {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
});

در کد بالا یک آرایه از لیست از نوع int تعریف کرده و در مرحله بعد بوسیله متد ForEach در کلاس Parallel اعضای لیست را پردازش می کنیم، با هر بار اجرا خروجی های متفاوتی دریافت خواهیم کرد:

۸
۵
Thread Id: 6
۴
۲
Thread Id: 1
۶
۳
Thread Id: 5
۹
Thread Id: 5
۱۰
Thread Id: 5
۱
Thread Id: 5
Thread Id: 4
Thread Id: 6
۶
Thread Id: 1
Thread Id: 3

همانطور که مشاهده می کنید شناسه های مربوط به thread در هر بار اجرای کدی مشخص شده در متد ForEach با یکدگیر متفاوت است، دلیل این موضوع ایجاد و مدیریت Thread ها توسط CLR Thread Pool است که ممکن است با هر بار فراخوانی متد مشخص شده به عنوان پارامتر دوم یک thread جدید ایجاد شده یا عملیات در یک thread موجود انجام شود.

کار با متد For

اما علاوه بر متد ForEach متد For نیست را می توان برای پردازش یک مجموعه استفاده کرد. در ساده ترین حالت این متد یک عدد به عنوان اندیس شروع حلقه، عدد دوم به عنوان اندیس پایان حلقه و یک پارامتر که متدی با پارامتر ورودی از نوع int یا long که نشان دهنده اندیس جاری است قبول می کند، برای مثال در متد زیر بوسیله متد For لیست numbers را در خروجی چاپ می کنیم، اما نه همه خانه های آن را پس عبارت اند از:

var numbers = new List &lt; int &gt;  {2, 6, 8, 1, 3, 9, 6, 10, 5, 4};
Parallel.For(3, 6, index  = &gt;
{
    Console.WriteLine(numbers[index]);
    Console.WriteLine("Thread Id: {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
});

با اجرای کد بالا خروجی زیر نمایش داده می شود، البته با هر بار اجرا ممکن است خروجی ها با هم متفاوت باشند:

۱
Thread Id: 1
۹
۳
Thread Id: 3
Thread Id: 4

یکی از کاربردی ترین موارد برای استفاده از کلاس Parallel و متدهای For و ForEach زمانی است که قصد داریم مجموعه حجیمی از اطلاعات را پردازش کنیم و البته پردازش هر المان وابسته به سایر المان ها نیست، زیرا عملیات پردازش المان ها به دلیل اینکه در Thread های مختلف انجام می شوند، ترتیبی در زمان اجرای المان ها در نظر گرفته نشده و ممکن است آیتمی در وسط لیست قبل از آیتم ابتدای لیست پردازش شود.

برای مثال، فرض کنید قصد دارید لیستی از تصاویر را گرفته و بر روی آن ها پردازشی انجام دهید یا لیستی از فایل ها را می خواهیم پردازش کنید، در اینجور مواقع به راحتی می توان از کلاس Parallel و متدهای آن استفاده کرد. یکی از مزیت های استفاده از کلاس Task این است که علاوه بر توزیع انجام کارها در میان Thread ها، در صورت موجود بودن بیش از یک CPU در سیستم شما، از سایر CPU ها هم برای پردازش اطلاعات استفاده می کند. در قسمت بعدی در مورد کلاس Task صحبت خواهیم کرد.

منبع



قسمت اول آموزش-برنامه نویسی Asynchronous – آشنایی با Process ها، Thread ها و AppDomain ها

قسمت دوم آموزش- آشنایی با ماهیت Asynchronous در Delegate ها

قسمت سوم آموزش-آشنایی با فضای نام System.Threading و کلاس Thread

قسمت چهارم آموزش- آشنایی با Thread های Foreground و Background در دات نت

قسمت پنجم آموزش- آشنایی با مشکل Concurrency در برنامه های Multi-Threaded و راهکار های رفع این مشکل

قسمت ششم آموزش- آشنایی با کلاس Timer در زبان سی شارپ

قسمت هفتم آموزش-آشنایی با CLR ThreadPool در دات نت

قسمت هشتم آموزش- مقدمه ای بر Task Parallel Library و کلاس Parallel در دات نت

قسمت نهم آموزش- برنامه نویسی Parallel:آشنایی با کلاس Task در سی شارپ

قسمت دهم آموزش-برنامه نویسی Parallel در سی شارپ :: متوقف کردن Task ها در سی شارپ – کلاس CancellationToken

قسمت یازدهم آموزش- برنامه نویسی Parallel در سی شارپ :: کوئری های Parallel در LINQ

قسمت دوازدهم آموزش- آشنایی با کلمات کلیدی async و await در زبان سی شارپ

قسمت سیزدهم آموزش- استفاده از متد WhenAll برای اجرای چندین Task به صورت همزمان در سی شارپ

برنامه نویسی Parallel در سی شارپ :: کوئری های Parallel در LINQ

علاوه بر مواردی که تا کنون پیرامون برنامه نویسی Parallel در دات نت آموختیم امکان نوشتن کوئری های LINQ به صورت Parallel نیز وجود دارد. این قابلیت بوسیله یکسری Extension Method که برای این موضوع تعریف شده امکان پذیر است و اصطلاحاً به کوئری های LINQ که به صورت Parallel اجرا می شوند PLINQ گفته می شود.
اما شیوه اجرای کوئری ها به صورت Parallel چگونه است؟ زمانی که شما از متدهای مربوطه برای اجرای کوئری ها به صورت Parallel استفاده می کنید، PLINQ کوئری مورد نظر را آنالیز کرده و تصمیم میگیرد که اجرای کوئری به صورت Parallel بر روی Performance برنامه شما تاثیر منفی می گذارد یا خیر.

کوئری که به صورت Parallel اجرا نشود اصطلاحاً Sequentional گفته می شود و PLINQ به صورت هوشمند نوع اجرای مناسب را برای کوئری شما انتخاب می کند، این موضوع به این معنی است که درخواست اجرای کوئری به صورت Parallel لزوماً تضمینی بر اجرای آن به صورت Parallel نیست و تصمیم این موضوع با آنالیزوری است که کوئری را تحلیل می کند.
متدهای مورد نیاز برای اجرای کوئری ها به صورت Parallel در کلاسی به نام ParallelEnumerable که در فضای نام System.LINQ قرار گرفته تعریف شده اند. در زیر ابتدا نگاهی به این متدها می اندازیم:

  1. متد AsParallel: این متد مشخص می کند که کوئری های بعد از این متد می بایست به صورت Parallel اجرا شوند.
  2. متد WithCancellation: مشخص می کند که در زمان اجرای کوئری به صورت دائم می بایست وضعیت token مشخص شده بررسی شده و در صورت درخواست Cancel کردن کوئری، روند اجرای آن متوقف شود.
  3. متد WithDegreeOfParallelism: مشخص می کند که چه تعداد پردازشگر باید برای اجرای کوئری تخصیص داده شود.
  4. متد ForAll: برای خروجی وضعیتی را فعال می کند که خروجی قبل از ادغام و بازگرداندن آن به Thread اصلی می بایست به صورت Parallel مورد پردازش تکمیلی قرار گیرد، دقیقاً مانند زمانی که نتیجه خروجی یک کوئری LINQ بوسیله دستور foreach مورد پردازش قرار می گیرد.

برای اینکه به صورت عملی با نحوه نوشتن کوئری های LINQ به صورت Parallel آشنا شویم مثال زیر را در نظر بگیرید:

static void Main(string[] args)
{
    Task.Factory.StartNew(ProcessData);

    Console.ReadKey();
}

public static void ProcessData()
{
    var source = Enumerable.Range(1, 1000000).ToArray();

    var evenNumbers = (from number in source where number%2 == 0 orderby number descending select number).ToArray();

    Console.WriteLine("Found {0} even numbers in source.", evenNumbers.Length);
}

در مثال بالا و در متد ProcessData، ابتدا بوسیله متد Range از کلاس Enumerable یک آرایه از اعداد ۱ تا ۱۰۰۰۰۰۰ ایجاد کردیم و در قدم بعدی بوسیله یک کوئری LINQ اعداد زوج را بدست آوردیم و در نهایت در خروجی تعداد اعداد زوج که بوسیله کوئری استخراج شده اند را در خروجی نمایش دادیم. اما اگر بخواهیم کوئری بالا به صورت Parallel اجرا شود می بایست بوسیله کوئری LINQ را به صورت زیر تغییر دهیم و از متد AsParallel استفاده کنیم:

var evenNumbers = (from number in source.AsParallel() where number%2 == 0 orderby number descending select number).ToArray();

همانطور که مشاهده می کنید، بعد از نوشتن منبع یعنی source از متد AsParallel برای اجرای کوئری به صورت Parallel استفاده شده است.

Cancel کردن یک کوئری PLINQ

برای اینکه به کاربر قابلیت کنسل کردن کوئری ها را بدهیم از متد WithCancellation استفاده می کنیم، در ابتدا باید یک شئ از CancellationTokenSource ایجاد کنیم:

private static CancellationTokenSource tokenSource = new CancellationTokenSource();

در قدم بعدی به صورت زیر که در قسمت قبلی آموختیم مکانیزم کنسل کردن کوئری را آموزش می دهیم:

static void Main(string[] args)
{
    Task.Factory.StartNew(ProcessData);
    Console.CancelKeyPress += (sender, eventArgs) = &gt;
    {
        eventArgs.Cancel = true;
        tokenSource.Cancel();
    };
    Console.ReadKey();
}

و در نهایت کد مربوط به کوئری را به صورت زیر تغییر می دهیم:

public static void ProcessData()
{
    var source = Enumerable.Range(1, 1000000).ToArray();

    int[] evenNumbers;

    try
    {
        evenNumbers = (from number in source.AsParallel().WithCancellation(tokenSource.Token) where number%2 == 0 orderby number descending select number).ToArray();
        Console.WriteLine("Found {0} even numbers in source.", evenNumbers.Length);
    }
    catch (OperationCanceledException ex)
    {
        Console.WriteLine(ex);
    }

}

همانطور که مشاهده می کنید در قسمت کوئری و بعد از متد AsParallel از متد WithCancellation برای مشخص کردن token تعریف شده که بواسطه آن کوئری را کنسل می کنیم استفاده کردیم. همچنین قسمت کوئری اصلی را داخل بدنه try..catch گذاشتیم، زیرا در صورت فراخوانی متد Cancel برای شئ tokenSource، کوئری به صورت خودکار خطای OperationCanceledException را تولید می کند (با این مکانیزم در قسمت قبلی آموزش آشنا شدیم.)

با اتمام این بخش مباحث مرتبط با TPL در دات نت نیز به پایان رسید، البته مطالب مرتبط با TPL بسیار گسترده بوده و شاید برای پوشش کل مباحث مرتبط نیاز به نوشتن صدها صفحه مطلب باشد، اما کلیت موضوع را سعی کردیم در این سری مطالب مطرح کنیم.

در قسمت بعدی که قسمت نهایی سری مطالب Asynchronous Programming است با کلمات کلیدی async و await که روش ساده تری برای نوشتن برنامه های Asynchronous در اختیار ما قرار می دهند آشنا می شویم.

منبع


قسمت اول آموزش-برنامه نویسی Asynchronous – آشنایی با Process ها، Thread ها و AppDomain ها

قسمت دوم آموزش- آشنایی با ماهیت Asynchronous در Delegate ها

قسمت سوم آموزش-آشنایی با فضای نام System.Threading و کلاس Thread

قسمت چهارم آموزش- آشنایی با Thread های Foreground و Background در دات نت

قسمت پنجم آموزش- آشنایی با مشکل Concurrency در برنامه های Multi-Threaded و راهکار های رفع این مشکل

قسمت ششم آموزش- آشنایی با کلاس Timer در زبان سی شارپ

قسمت هفتم آموزش-آشنایی با CLR ThreadPool در دات نت

قسمت هشتم آموزش- مقدمه ای بر Task Parallel Library و کلاس Parallel در دات نت

قسمت نهم آموزش- برنامه نویسی Parallel:آشنایی با کلاس Task در سی شارپ

قسمت دهم آموزش-برنامه نویسی Parallel در سی شارپ :: متوقف کردن Task ها در سی شارپ – کلاس CancellationToken

قسمت یازدهم آموزش- برنامه نویسی Parallel در سی شارپ :: کوئری های Parallel در LINQ

قسمت دوازدهم آموزش- آشنایی با کلمات کلیدی async و await در زبان سی شارپ

قسمت سیزدهم آموزش- استفاده از متد WhenAll برای اجرای چندین Task به صورت همزمان در سی شارپ