کار با Thread ها در زبان سی شارپ – آشنایی با Thread های Foreground و Background در دات نت

زمانی که یک Thread جدید در برنامه های دات نت ایجاد می شوند، این Thread ها می توانند به دو صورت Foreground و Background اجرا شوند:

  1. Thread های Foreground: زمانی که کی Thread در حالت Foreground اجرا می شود باعث می شود که Thread اصلی برنامه تا زمان کامل شدن اجرای Thread ایجاد شده در حالت اجرا بماند. یعنی از Shut-down شدن Primary Thread توسط CLR جلوگیری می شود.
  2. Thread های Background: این Thread ها که با نام Daemon Thread شناخته می شوند به CLR می گوید که اجرای این Thread آنقدر اهمیت ندارد که Thread اصلی برنامه بخواهد منتظر بماند تا عملیات آن به اتمام برسد و می تواند در هر زمان که Thread اصلی برنامه به اتمام رسید، به صورت خودکار Thread های Background را نیز از بین ببرد.

توجه کنید که کلیه Thread هایی که در برنامه ها ایجاد می کنیم به صورت پیش فرض در حالت Foreground قرار دارند. برای آشنایی بیشتر با این موضوع نمونه کد زیر را در نظر بگیرید:

static void Main(string[] args)
{
    var thread = new Thread(PrintNumbers);
    thread.Start();
}

public static void PrintNumbers()
{
    for (int counter = 1; counter < 10; counter++)
    {
        Console.WriteLine(counter);
        Thread.Sleep(200);
    }
}

همانطور که گفتیم Thread ایجاد شده به صورت پیش فرض از نوع Foreground است و به همین دلیل تا زمانی که روند اجرای Thread ایجاد شده به اتمام نرسد از برنامه خارج نمی شویم و کلیه اعداد در خروجی چاپ می شوند. اما در کد زیر Thread ایجاد شده به صورت Background است و خواهیم دید که پس از اجرای برنامه به دلیل اینکه Thread اصلی زودتر از Thread ایجاد شده به اتمام می رسد، CLR به صورت خودکار Thread ایجاد شده را از بین می برد و اعداد به صورت کامل در خروجی نمایش داده نمی شوند:

static void Main(string[] args)
{
    var thread = new Thread(PrintNumbers);
    thread.IsBackground = true;
    thread.Start();
}

public static void PrintNumbers()
{
    for (int counter = 1; counter < 10; counter++)
    {
        Console.WriteLine(counter);
        Thread.Sleep(200);
    }
}

در برنامه های واقعی باید با دقت نوع Thread ها را انتخاب کرد، برای مثال فرض کنید که در برنامه شما در یک Thread جداگانه عملیاتی بر روی داده های بانک اطلاعاتی انجام می شود و نتیجه این عملیات در انتها باید در جایی ذخیره شود، می توانید برای اینکار یک Thread از نوع Foreground ایجاد کرده تا پس از خروج از برنامه، Thread اصلی منتظر اتمام انجام عملیات شده و سپس عملیات خروج کامل انجام شود. در مبحث بعدی در مورد موضوع همزمانی یا Concurrency صحبت می کنیم که از مشکلات اساسی در زمینه برنامه نویسی 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 به صورت همزمان در سی شارپ

به خاطر دارید که Delegate نوع های داده ای بودند که اطلاعات مربوط به یک متد را در خود نگهداری می کردند؟ زمانی که یک delegate جدید تعریف می کنید، در حقیقت کلاس جدیدی ایجاد می شود که این کلاس، از کلاس MultiCastDelegate مشتق شده است. این موضوع باعث می شود که delegate تعریف شده شامل یکسری متدها باشد. قبلاً با delegate ها و شیوه فراخوانی آن ها آشنا شدیم. در این مطلب می خواهیم با شیوه فراخوانی متدها به صورت Asynchronous آشنا شویم. زمانی که از روی یک delegate شئ ای برای یک متد ایجاد می کنیم، این شئ شامل دو متد به نام های زیر است:

  1. BeginInvoke
  2. EndInvoke

از این دو متد می توان برای فراخوانی delegate ها به صورت Asynchronous استفاده کرد. اگر به خاطر داشته باشید، delegate ها یک متد دیگر نیز داشتند به نام Invoke که به وسیله این متد می توانستیم delegate را به صورت عادی فراخوانی کنیم. تفاوت Invoke و BeginInvoke در این است که متد Invoke عملیات فراخوانی را در Thread جاری انجام می دهد، اما متد BeginInvoke یک Thread جدید ایجاد کرده و delegate را در آن Thread فراخوانی می کند. برای آشنایی بیشتر با یک مثال جلو می رویم، Delegate ای به صورت زیر تعریف می کنیم:

public delegate int MathOperation(int n1, int n2);

در ادامه کد زیر را نیز اضافه می کنیم:

static void Main(string[] args)
{
    MathOperation operation = new MathOperation(Add);
    Console.WriteLine("Main thread ID:"+Thread.CurrentThread.ManagedThreadId);
    operation(2, 6);
}

public static int Add(int n1, int n2)
{
    Console.WriteLine("Add thread ID: " + Thread.CurrentThread.ManagedThreadId);
    return n1 + n2;
}

به خطوط 4 و 10 دقت کنید، در این خطوط از کلاس Thread استفاده کردیم، کلاس Thread یک Property دارد به نام CurrentThread که اطلاعات Thread جاری که متد در آن اجرا شده است را بر میگرداند، خاصیت CurrentThread از نوع کلاس Thread است که بوسیله خاصیت ManagedThreadId می توان شناسه Thread در حال اجرا را بدست آورد. بعد اجرای کد بالا با خروجی زیر مواجه می شویم:

Main thread ID:1
Add thread ID: 1

همانطور که مشاهده می کنید شناسه Thread بر هر دو متد Main و Add یکسان است، اما همانطور که گفتیم می خواهیم متد Add را در یک Thread جداگانه اجرا کنیم. در اینجا از متد BeginInvoke که برای Delegate تعریف شده استفاده می کنیم. برای MathOperation متد BeginInvoke به صورت زیر تعریف شده :

IAsyncResult BeginInvoke(int n1, int n2, AsyncCallBack callback, object state);

همانطور که مشاهده می کنید پارامترهای اول و دوم تعریف شده برای BeginInvoke مبتنی بر پارامترهایی است که برای Delegate تعریف کردیم. پارامترهای callback و state را هم بعداً بررسی می کنیم، در حال حاضر برای فراخوانی متد BeginInvoke برای این دو پارامتر مقدار null ارسال می کنیم.

همچنین متد EndInvoke نیز برای MathOperation به صورت زیر تعریف شده است:

int EndInvoke(IAsyncResult result);

همانطور که مشاهده می کنید مقدار بازگشتی EndInvoke از نوع int است که بر اساس نوع بازگشتی delegate تعریف شده مشخص می شود. همچنین پارامتر ورودی EndInvoke از نوع IAsyncResult است که در ادامه به بررسی این interface خواهیم پرداخت.

اینترفیس IAsyncResult

همانطور که مشاهده کردید، مقدار بازگشتی متد BeginInvoke از نوع IAsyncResult است. این interface در متدهای BeginInvoke و EndInvoke استفاده می شود، در متد BeginInvoke مقدار بازگشتی شئ ای از نوع IAsyncResult است و در متد EndInvoke پارامتر ورودی این متد از نوع IAsyncResult می باشد. تعریف IAsyncResult به صورت زیر است:

public interfae IAsyncResult
{
    object AsyncState { get; set; }
    WaitHandle AsyncWaitHandle { get; set; }
    bool CompletedSynchronously { get; set; }
    bool IsCompleted { get; set; }
}

در ساده ترین حالت ممکن شما نیازی به کار با اعضاء این اینترفیس ندارید، تنها کاری که باید بکنید نگهداری مقدار بازگردانده شده از متد BeginInvoke و ارسال آن به متد EndInvoke در زمان مناسب برای گرفتن خروجی است. برای آشنایی بیشتر با متدهای BeginInvoke و EndInvoke و همچنین استفاده از IAsyncResult با یک مثال ساده جلو می رویم. کدی که برای MathOperation در ابتدای این مطلب نوشتیم را به صورت زیر تغییر می دهیم:

MathOperation operation = new MathOperation(Add);
Console.WriteLine("Main thread ID:"+Thread.CurrentThread.ManagedThreadId);
var result = operation.BeginInvoke(2, 6, null, null);
Console.WriteLine("Task in Main method.");
var answer = operation.EndInvoke(result);
Console.WriteLine("2 + 6 = {0}", answer);

بعد از اجرای کد بالا، خروجی به صورت زیر خواهد بود:

Main thread ID:1
Task in Main method.
Add thread ID: 3
2 + 6 = 8

همانطور که مشاهده می کنید، شناسه Thread برای متد Add مقدار 3 می باشد، به این معنی که متد Add در حال اجرا در یک Thread جداگانه از Thread اصلی برنامه یا Main Thread است. اما یک موضوع هنوز باقی مانده، Synchronization. به متد Main دقت کنید، زمانی که delegate با متد BeginInvoke فراخوانی می شود و بعد از آن پیغام Task in Main method را در خروجی چاپ می کنیم، در حقیقت Thread جاری برای لحظاتی متوقف شده و بعد بوسیله متد EndInvoke خروجی را دریافت می کنیم. اما کاری که ما می خواهیم انجام دهیم همزمانی اجرای متد WriteLine و متد Add است، برای اینکار باید از اعضاء IAsyncResult استفاده کنیم. در این اینترفیس خصوصیتی تعریف شده با نام IsCompleted که در صورت اتمام اجرای متد مقدار true را بر میگرداند. کدی که نوشتیم را به صورت زیر تغییر می دهیم:

static void Main(string[] args)
{
    MathOperation operation = new MathOperation(Add);
    Console.WriteLine("Main thread ID:"+Thread.CurrentThread.ManagedThreadId);
    var result = operation.BeginInvoke(2, 6, null, null);
    while (!result.IsCompleted)
    {
        Console.WriteLine("Task in Main method...");
        Thread.Sleep(1000);
    }
    Console.WriteLine("Task in Main method.");
    var answer = operation.EndInvoke(result);
    Console.WriteLine("2 + 6 = {0}", answer);
}

public static int Add(int n1, int n2)
{
    Console.WriteLine("Add thread ID: " + Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(5000);
    return n1 + n2;
}

به متد Thread.Sleep دقت کنید، این متد روند اجرای Thread را برای مدت زمان مشخص شده متوقف می کند. عدد وارد شده به میلی ثانیه است. در کد بالا متد Add به میزان 5 ثانیه و متد با هر بار تکرار حلقه while متد متد Main به اندازه 1 ثانیه متوقف می شوند. اجرای کد بالا خروجی زیر را تولید می کند:

Main thread ID:1
Task in Main method...
Add thread ID: 3
Task in Main method...
Task in Main method...
Task in Main method...
Task in Main method...
2 + 6 = 8

با اینکه متد Add فراخوانی شده و Thread مرتبط با آن به مدت 5 ثانیه در حالت Sleep قرار گرفته، اما متد Main در حال انجام کار خودش است و تا زمانی که متد Add کامل نشده و IsCompleted در IAsyncResult مقدار true بر نگرداند دستور WriteLine فراخوانی شده و پیام Task in Main method بر روی صفحه نمایش داده می شود. در این کد ما از خاصیت IsCompleted استفاده کردیم، یکی دیگر از راه ها برای پیاده سازی حالت گفته شده استفاده از متد WaitOn است که در کلاس WaitHandle پیاده سازی شده است. خاصیت AsyncWaitHandle در IAsyncResult شئ ای از نوع WaitHandle بر می گرداند. یکی از مزیت های استفاده از این متد قابلیت مشخص کردن time out برای block کردن thread جاری است، یعنی دیگر نیازی به استفاده از Thread.Sleep نخواهیم داشت:

MathOperation operation = new MathOperation(Add);
Console.WriteLine("Main thread ID:"+Thread.CurrentThread.ManagedThreadId);
var result = operation.BeginInvoke(2, 6, null, null);
while (!result.AsyncWaitHandle.WaitOne(1000,true))
{
    Console.WriteLine("Task in Main method...");
}
var answer = operation.EndInvoke(result);
Console.WriteLine("2 + 6 = {0}", answer);

همانطور که مشاهده می کنید، برای متد WaitOn در حلقه while مقدار 1000 میلی ثانیه یا 1 ثانیه را مشخص کردیم. هر زمان که روند اجرای کد به این دستور می رسد، thread جاری به اندازه 1 ثانیه منتظر تکمیل اجرای متد Add شده و سپس وارد حلقه while می شود. در صورتی که کار متد Add به اتمام برسد، متد WaitOn مقدار true بر میگرداند. در مورد پارامتر دوم WaitOn که مقدار true به آن پاس داده شده در بخش های مرتبط با Synchronization Context صحبت خواهیم کرد.

استفاده از AsyncCallBack

تا اینجا گفتیم که چگونه می توان اجرای همزمان دو Thread را بوسیله Delegate ها پیاده سازی کرد، همچنین با نحوه کنترل دریافت خروجی از Thread های اجرا شده آشنا شدیم و گفتیم که بوسیله IAsyncResult می توان خروجی را دریافت کرد. اگر به خاطر داشته باشید زمان فراخوانی متد BeginInvoke برای دو پارامتر callback و state مقدار null ارسال کردیم. در این قسمت می خواهیم در مورد پارامتر callback صحبت کنیم، این پارامتر که یک Delegate از نوع AsyncCallBack قبول می کند، به ما این امکان را می دهد تا متدی را به متد BeginInvoke پاس دهیم. این delegate زمانی اجرا می شود که روند اجرای متدی که با BeginInvoke فراخوانی شده است به اتمام برسد. متدی که برای callback باید ارسال شود باید به مبتنی بر signature زیر باشد:

void CallBackMethod(IAsyncResult result)
{
     // code for callback
}

مثالی که تا این لحظه بر اساس آن جلو آمدیم را به صورت زیر تغییر می دهیم تا از قابلیت AsynCallBack استفاده کند:

private static bool isDone = false;

static void Main(string[] args)
{
    MathOperation operation = new MathOperation(Add);
    Console.WriteLine("Main thread ID:"+Thread.CurrentThread.ManagedThreadId);
    var result = operation.BeginInvoke(2, 6, new AsyncCallback(CallBack), null);
    while (!isDone)
    {
        Console.WriteLine("Task in Main method...");
        Thread.Sleep(1000);
    }
    var answer = operation.EndInvoke(result);
    Console.WriteLine("2 + 6 = {0}", answer);
}

public static void CallBack(IAsyncResult result)
{
    isDone = true;
}

همانطور که در کد بالا مشاهده می کنید در ابتدا یک فیلد با نام isDone تعریف شده که از آن برای مشخص کردن وضعیت اجرای Thread استفاده می کنیم. زمانی که متد BeginInvoke را فراخوانی می کنیم به عنوان پارامتر سوم شئ ای از نوع AsyncCallback که متد CallBack برای آن مشخص شده ارسال می شود، این متد زمانی فراخوانی می شود که روند اجرای Thread مرتبط با متد Add به اتمام برسد، متد CallBack پس از اجرا مقدار isDone را برابر true قرار می دهد و به همین دلیل از حلقه while تعریف شده در متد Main خارج می شویم.

همانطور که در کد بالا مشاهده می کنید پارامتر ورودی CallBack از نوع IAsyncResult است، یعنی می توان عملیات گرفتن خروجی را در داخل CallBack نیز انجام داد. برای اینکار کد بالا را به صورت زیر تغییر می دهیم:

private static bool isDone = false;

static void Main(string[] args)
{
    MathOperation operation = new MathOperation(Add);
    Console.WriteLine("Main thread ID:"+Thread.CurrentThread.ManagedThreadId);
    operation.BeginInvoke(2, 6, new AsyncCallback(CallBack), null);
    while (!isDone)
    {
        Console.WriteLine("Task in Main method...");
        Thread.Sleep(1000);
    }            
}

public static void CallBack(IAsyncResult result)
{
    AsyncResult asyncRes = (AsyncResult) result;
    var opDelegate = (MathOperation) asyncRes.AsyncDelegate;
    var answer = opDelegate.EndInvoke(result);
    Console.WriteLine("2 + 6 = {0}", answer);
    isDone = true;
}

تغییراتی که در کد بالا دادیم به ترتیب:

  1. ابتدا در متد CallBack پارامتر ورودی result را به کلاس AsyncResult تبدیل کردیم، کلاس AsyncResult اینترفیس IAsyncResult را پیاده سازی کرده است و علاوه بر اعضاء این اینترفیس یکسری اعضاء دیگر دارد از جمله AsyncDelegate که شئ delegate فراخوانی شده را برای ما بر می گرداند.
  2. در قدم بعدی AsyncDelegate را به MathOperation تبدیل کردیم.
  3. در انتها عملیاتی که در متد Main برای گرفتن خروجی نوشته بودیم را به متد CallBack منتقل کردیم تا بتوانیم خروجی متد Add را گرفته و در پنجره Console نمایش دهیم.
  4. در انتها مقدار isDone را برابر true قرار دادیم تا اطلاع دهیم عملیات اجرای متد به پایان رسیده است.

ارسال و دریافت داده های دلخواه بین Thread ها

در خاتمه این قسمت آموزشی با پارامتر چهارم متد BeginInvoke، یعنی state آشنا می شویم. بوسیله این پارامتر می توان یک مقدار یا شئ دلخواه را به Delegate ارسال و از آن بوسیله IAsyncResult استفاده کرد. برای مثال، می خواهیم زمانی که Delegate را فراخوانی می کنیم یک رشته را به delegate پاس داده و در متد CallBack آن را نمایش دهیم. کد نوشته شده را به صورت زیر تغییر می دهیم:

private static bool isDone = false;

static void Main(string[] args)
{
    MathOperation operation = new MathOperation(Add);
    Console.WriteLine("Main thread ID:"+Thread.CurrentThread.ManagedThreadId);
    operation.BeginInvoke(2, 6, new AsyncCallback(CallBack), "This is state passed to thread from Main Method!!");
    while (!isDone)
    {
        Console.WriteLine("Task in Main method...");
        Thread.Sleep(1000);
    }            
}

public static void CallBack(IAsyncResult result)
{
    Console.WriteLine("State: " + result.AsyncState);
    AsyncResult asyncRes = (AsyncResult) result;
    var opDelegate = (MathOperation) asyncRes.AsyncDelegate;
    var answer = opDelegate.EndInvoke(result);
    Console.WriteLine("2 + 6 = {0}", answer);
    isDone = true;
}

در متد و زمان فراخوانی delegate بوسیله BeginInvoke به عنوان پارامتر چهارم یک رشته را به عنوان state ارسال کرده و در CallBack بوسیله خاصیت AsyncState توانستیم state ارسال شده را گرفته و در خروجی نمایش دهیم.
تا این قسمت شما یاد گرفتید که چگونه در زبان دات نت می توان با کمک Delegate ها اقدام به اجرای کدها در یک Thread جداگانه کرد. در قسمت های بعدی آموزش با نحوه استفاده از کلاس Thread که در فضای نام System.Threading قرار دارد بیشتر آشنا خواهیم شد.

منبع


قسمت اول آموزش-برنامه نویسی 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 به صورت همزمان در سی شارپ

 

 

 

برنامه نویسی Asynchronous – آشنایی با Process ها، Thread ها و AppDomain ها

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

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

Process چیست؟

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

در حقیقت هر Process مرزبندی بین برنامه های اجرا است برای جدا سازی منابع و حافظه های تخصیص داده شده. دقت کنید که لزوماً تعداد Process برابر با تعداد برنامه های در حال اجرا نیست، یک برنامه می تواند یک یا چند Process را در زمان اجرا درگیر کند. در سیستم عامل ویندوز می توان از بخش Task Manager لیست برنامه های در حال اجرا و Process ها را مشاهده کرد. در تصویر زیر لیست برنامه هایی که بر روی سیستم من در حال اجرا است را مشاهده می کنید:

 

parallel programming

 

در صورتی که بر روی دکمه More details کلیک کنید می توانید از تب Processes لیست Process های در حال اجرا را مشاهده کنید:

parallel programming

هر یک Process های در حال اجرا حافظه، منابع تخصیص داده شده و روند اجرای مربوط به خود را دارند. در تصویر بالا نیز مشخص است، برای مثال در زمان گرفتن عکس بالا، Prcess مربوط به برنامه paint.net مقدار 20.4% از CPU و همچنین 107.6MB از حافظه را اشغال کرده است. در اینجا بیشتر به بحث CPU Usage باید دقت کنیم که نشان دهنده میزان استفاده یک Process از CPU است. CPU Usage در حقیقت یک ترتیب اجرا است که اصطلاحاً به آن Thread می گویند. هر Process می تواند شامل یک یا چندین Thread باشد که هر Thread وظیفه انجام یک عملیات خاص را بر عهده دارد. اما زمان اجرای هر Process یک Thread اولیه اجرا می شود که به آن اصطلاحاً Main Thread گفته می شود.

Process های Multi-Thread

همانطور که گفتیم هر Process می تواند شامل یک یا چندین Thread باشد. این Thread ها تنها توسط Process ایجاد نمی شوند و افرادی که اقدام به ایجاد نرم افزار می کنند (همان برنامه نویس های معروف) نیز می توانند برای انجام عملیات های مورد نظر اقدام به ایجاد Thread کنند.

برای مثال محیط Visual Studio را در نظر بگیرید، این محیط در طول زمان نوشتن کدها عملیات های دیگری را نیز برای شما انجام می دهد، مانند پردازش سایر فایل های پروژه، رنگی کردن کدها، نمایش Intellisense و …، اما شما احساس می کنید که تمامی این کار به صورت همزمان انجام می شوند، به این دلیل که برای هر یک از این عملیات ها یک Thread جداگانه ایجاد می شود که تمامی این Thread ها در Prcess مربوط به Visual Studio در حال اجرا هستند و به همین دلیل شما نباید منتظر بمانید تا عملیات های در حال اجرا به اتمام برسند و می توانید کار خود را ادامه دهید.

Thread های در حال اجرا در یک Process نیاز به یکسری اطلاعات در مورد Process دارند تا بتوانند به کار خود ادامه دهند. به این اطلاعات اصطلاحاً Prcess Global Data یا داده های عمومی یک پراسس می گویند. اگر بخواهیم نمایی کلی از یک Process را نشان دهیم می توان شکل زیر را مثال زد:

parallel programming

تصویر بالا، یک Process را نشان می دهد که شامل دو Thread است، هر یک از این Thread ها یک وظیفه خاص را انجام می دهند، اما به سکری اطلاعات دسترسی دارند که به آن ها PGD یا Process Global Data می گویند.

تا اینجا با دو مفهوم Process و Thread آشنا شدیم، اما همانطور که گفتیم در زبان سی شارپ می توانیم Thread هایی ایجاد کنیم که هر Thread یک کار خاص را انجام می دهد. برای کار با Thread ها و برای شروع، با کلاسی به نام Thread که در فضای نام System.Threading قرار دارد کار می کنیم. به صورت زیر می توانیم یک thread جدید با استفاده از کلاس Thread ایجاد کرده و آنرا اجرا کنیم:

static void Main(string[] args)
{
    var thread1 = new Thread(Thread1Job);
    var thread2 = new Thread(Thread2Job);
    var thread3 = new Thread(Thread3Job);
    thread1.Start();
    thread2.Start();
    thread3.Start();
}

public static void Thread1Job()
{
    for (int counter = 0; counter < 50; counter++)
    {
   Console.WriteLine("From thread1: " + counter);
    }
}

public static void Thread2Job()
{
    for (int counter = 0; counter < 50; counter++)
    {
        Console.WriteLine("From thread2: " + counter);
    }
}

public static void Thread3Job()
{
    for (int counter = 0; counter < 50; counter++)
    {
        Console.WriteLine("From thread3: " + counter);
    }
}

همانطور که مشاهده می کنید در کد بالا 3 شئ از نوع Thread ایجاد کردیم و برای پارامتر Constructor متد مورد نظر را ارسال کردیم. Constructor کلاس Thread پارامترش از نوع Delegate است و به همین دلیل می توان یک متد را جهت اجرا در Thread به عنوان پارامتر به آن ارسال کرد. بعد از تعریف thread ها به ترتیب آن را بوسیله متد Start اجرا می کنیم. در تصویر زیر خروجی کد بالا را مشاهده می کنید که کد های Thread ها به صورت همزمان اجرا شدند:

parallel programming

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

منبع


قسمت اول آموزش-برنامه نویسی 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 به صورت همزمان در سی شارپ

 

 

کار با Thread ها در زبان سی شارپ – آشنایی با فضای نام System.Threading و کلاس Thread

تا اینجا متوجه شدیم که چگونه می توان با کمک Delegate ها کدها را در یک Thread جداگانه و به صورت Asynchrnonous اجرا کرد. در ادامه مباحث مرتبط با برنامه نویسی Asynchronous به سراغ فضای نام System.Threading می رویم. این فضای نام شامل یکسری کلاس است که روند نوشتن برنامه Multi-Threaded را آسان می کند. کلاس های زیادی در این فضای نام وجود دارد که هر یک استفاده خاص خودش را دارد. در زیر با توضیح اولیه برخی از کلاس های این فضای نام آشنا می شویم:

  1. Interlocked: از این کلاس برای اجرای عملیات های atomic یا Atomic Operations بر روی متغیرهایی که در بین چندین Thread به اشتراک گذاشته شدند استفاده می شود.
  2. Monitor: از این کلاس برای پیاده سازی Synchronization بر روی اشیاء ای که Thread به آن دسترسی دارند استفاده می شود. در سی شارپ کلمه کلیدی lock در پشت زمینه از مکانیزم Monitor برای Synchronization استفاده می کنید که در بخش های بعدی با این تکنیک بیشتر آشنا می شویم.
  3. Mutex: از این کلاس برای اعمال Synchronization بین AppDomain ها استفاده می شود.
  4. ParameterizedThreadStart: این delegate به thread ها این اجازه را می دهد تا متدهایی پارامتر ورودی دارند را فراخوانی کند.
  5. Semaphor: از این کلاس برای محدود کردن تعداد Thread هایی که می توانند به یک Resource دسترسی داشته باشند استفاده می شود.
  6. Thread: به ازای هر Thread ایجاد شده برای برنامه باید یک کلاس از نوع Thread ایجاد کرد. در حقیقت کلاس Thread نقش اصلی را در ایجاد و استفاده از Thread ها دارد.
  7. ThreadPool: بوسیله این کلاس می توان به Thread-Pool مدیریت شده توسط خود CLR دسترسی داشت.
  8. ThreadPriority: بوسیله این enum می توان درجه اهمیت یک Thread را مشخص می کند.
  9. ThreadStart: از این delegate برای مشخص کردن متدی که در Thread باید اجرا شود استفاده می شود. این delegate بر خلاف ParameterizedThreadStart پارامتری قبول نمیکند.
  10. ThreadState: بوسیله این enum می توان وضعیت جاری یک thread را مشخص کرد.
  11. Timer: بوسیله کلاس Timer می توان مکانیزمی پیاده کرد که کدهای مورد نظر در بازه های زمانی خاص، مثلاً هر 5 ثانیه یکبار و در یک Thread مجزا اجرا شوند.
  12. TimerCallBack: از این delegate برای مشخص کردن کدی که داخل timer باید اجرا شود استفاده می شود.

کلاس System.Threading.Thread

کلاس Thread اصلی ترین کلاس موجود در فضای نام System.Threading است. از یک کلاس برای دسترسی به Thread هایی که در روند اجرای یک AppDomain ایجاد شده اند استفاده می شود. همچنین بوسیله این کلاس می تواند Thread های جدید را نیز ایجاد کرد. کلاس Thread شامل یکسری متد و خصوصیت است که در این قسمت می خواهیم با آن ها آشنا شویم. ابتدا به سراغ خصوصیت CurrentThread که یک خصوصیت static در کلاس Thread است می رویم. بوسیله این خصوصیت می توان اطلاعات Thread جاری را بدست آورد. برای مثال در صورتی که در متد Main از این خصوصیت استفاده شود می توان به اطلاعات مربوط به Thread اصلی برنامه دسترسی داشت یا اگر برای یک متد Thread جداگانه ای ایجاد شود، در صورت استفاده از این خصوصیت در بدنه متد به اطلاعات Thread ایجاد شده دسترسی خواهیم داشت. در ابتدا با یک مثال می خواهیم اطلاعات Thread اصلی برنامه را بدست آوریم:

var primaryThread = Thread.CurrentThread;
primaryThread.Name = "PrimaryThread";
Console.WriteLine("Thread Name: {0}", primaryThread.Name);
Console.WriteLine("Thread AppDomain: {0}", Thread.GetDomain().FriendlyName);
Console.WriteLine("Thread Context Id: {0}", Thread.CurrentContext.ContextID);
Console.WriteLine("Thread Statred: {0}", primaryThread.IsAlive);
Console.WriteLine("Thread Priority: {0}", primaryThread.Priority);
Console.WriteLine("Thread State: {0}", primaryThread.ThreadState);

دقت کنید در ابتدا با به Thread یک نام دادیم. در صورتی که نامی برای Thread انتخاب نشود خصوصیت Name مقدار خالی بر میگرداند. مهمترین مزیت تخصیص نام برای Thread راحت تر کردن امکان debug کردن کد است. در Visual Studio پنجره ای وجود دارد به نام Threads که می توانید در زمان اجرای برنامه از طریق منوی Debug->Windows->Threads به آن دسترسی داشته باشید. در تصویر زیر نمونه ای از این پنجره را در زمان اجرا مشاهده می کنید:

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

اما بریم سراغ موضوع اصلی، یعنی ایجاد Thread و اجرای آن. در ابتدا در مورد مراحل ایجاد یک Thread صحبت کنیم، معمولاً برای ایجاد یک Thread مراحل زیر را باید انجام دهیم:

  1. در ابتدا باید متدی ایجاد کنیم که وظیفه آن انجام کاری است که قرار است در یک Thread جداگانه انجام شود.
  2. در مرحله بعد باید یکی از delegate های ParameterizedThreadStart برای متدهایی که پارامتر ورودی دارند یا ThreadStart برای متدهای بدون پارامتر را انتخاب کرده و یک شئ از آن ایجاد کنیم که به عنوان سازنده متد مورد نظر به آن پاس داده می شود.
  3. از روی کلاس Thread یک شئ جدید ایجاد کرده و به عنوان سازنده شئ ای که از روی delegate های گفته شده در مرحله 2 ساختیم را به آن ارسال کنیم.
  4. اطلاعات و تنظیمات اولیه مورد نظر برای Thread مانند Name یا Priority را برای آن ست کنیم.
  5. متد Start را در کلاس Thread را برای شروع کار Thread فراخوانی کنیم. با این کار متدی که در مرحله 2 مشخص کردیم در یک Thread جداگانه اجرا می شود.

دقت کنید در مرحله 2 می بایست بر اساس signature متدی که قصد اجرای آن در thread جداگانه را داریم، delegate مناسب انتخاب شود. همچنین ParameterizedThreadStart پارامتری که به عنوان ورودی قبول می کند از نوع Object است، یعنی اگر می خواهید چندین پارامتر به آن ارسال کنید می بایست حتماً یک کلاس یا struct ایجاد کرده و آن را به عنوان ورودی به کلاس Start ارسال کنید. با یک مثال ساده که از ThreadStart برای اجرای Thread استفاده می کند شروع می کنیم:

static void Main(string[] args)
{
    ThreadStart threadStart = new ThreadStart(PrintNumbers);
    Thread thread = new Thread(threadStart);
    thread.Name = "PrintNumbersThread";
    thread.Start();
    while (thread.IsAlive)
    {
        Console.WriteLine("Running in primary thread...");
        Thread.Sleep(2000);
    }
    Console.WriteLine("All done.");
    Console.ReadKey();
}

public static void PrintNumbers()
{
    for (int counter = 0; counter < 10; counter++)
    {
        Console.WriteLine("Running from thread: {0}", counter + 1);
        Thread.Sleep(500);
    }
}

در ابتدا متدی تعریف کردیم با نام PrintNumbers که قرار است در یک Thread مجزا اجرا شود. همانطور که مشاهده می کنید این متد نه پارامتر ورودی دارد و نه مقدار خروجی، پس از ThreadStart استفاده می کنیم. بعد از ایجاد شئ از روی ThreadStart و ایجاد Thread، نام Thread را مشخص کرده و متد Start را فراخوانی کردیم. به حلقه while ایجاد شده دقت کنید، در این حلقه بوسیله خصوصیت IsAlive گفتیم تا زمانی که Thread ایجاد شده در حال اجرا است کد داخل while اجرا شود. همچنین بوسیله متد Sleep در متد Main و متد PrintNumbers در عملیات اجرا برای Thread های مربوط به متد تاخیر ایجاد کردیم. بعد اجرای کد بالا خروجی زیر نمایش داده می شود:

Running in primary thread...
Running from thread: 1
Running from thread: 2
Running from thread: 3
Running from thread: 4
Running in primary thread...
Running from thread: 5
Running from thread: 6
Running from thread: 7
Running from thread: 8
Running in primary thread...
Running from thread: 9
Running from thread: 10
All done.

در قدم بعدی فرض کنید که قصد داریم بازه اعدادی که قرار است در خروجی چاپ شود را به عنوان پارامتر ورودی مشخص کنیم، در اینجا ابتدا یک کلاس به صورت زیر تعریف می کنیم:

public class PrintNumberParameters
{
    public int Start { get; set; }
    public int Finish { get; set; }
}

در قدم بعدی کلاس PrintNumbers را به صورت زیر تغییر می دهیم:

public static void PrintNumbers(object data)
{
    PrintNumberParameters parameters = (PrintNumberParameters) data;
    for (int counter = parameters.Start; counter < parameters.Finish; counter++)
    {
        Console.WriteLine("Running from thread: {0}", counter);
        Thread.Sleep(500);
    }
}

همانطور که مشاهده می کنید، پارامتر ورودی PrintNumbers از نوع object است و در بدنه ورودی را به کلاس PrintNumberParameters تبدیل کرده و از آن استفاده کردیم. در مرحله بعد متد Main را باید تغییر داده و به جای ThreadStart از ParameterizedThreadStart استفاده کنیم، همچنین به عنوان پارامتر ورودی برای متد Start شئ ای از PrintNumberParameters ایجاد کرده و با عنوان پارامتر به آن ارسال می کنیم:

ParameterizedThreadStart threadStart = new ParameterizedThreadStart(PrintNumbers);
Thread thread = new Thread(threadStart);
thread.Name = "PrintNumbersThread";
thread.Start(new PrintNumberParameters() {Start = 5, Finish = 13});
while (thread.IsAlive)
{
    Console.WriteLine("Running in primary thread...");
    Thread.Sleep(2000);
}
Console.WriteLine("All done.");
Console.ReadKey();

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

منبع


قسمت اول آموزش-برنامه نویسی 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 به صورت همزمان در سی شارپ

موازی سازی(Parallelism) چیست؟

ﺷﺮﮐﺖ ﻫﺎی ﺗﻮﻟﯿﺪ ﮐﻨﻨﺪه ﭘﺮدازﺷﮕﺮ ﺑﺮای اﻓﺰاﯾﺶ ﺳﺮﻋﺖ ﭘﺮدازﻧﺪه ﻣﺠﺒﻮر ﺑﻪ ﺑﺎﻻﺑﺮدن ﻓﺮﮐﺎﻧﺲ ﭘﺮدازﺷﮕﺮ ﺑﻮدﻧﺪ. ﯾﮏ راه اﻓﺰاﯾﺶ وﻟﺘﺎژ ﻣﺼﺮﻓﯽ ﭘﺮدازﻧﺪه ﺑﻮد ﮐﻪ دارای ﻧﻘﺎط ﺿﻌﻔﯽ ﻣﺎﻧﻨﺪ اﻓﺰاﯾﺶ دﻣﺎ و اﻓﺰاﯾﺶ ﻣﺼﺮف ﺑﺎﻃﺮی ﻧﯿﺰ ﺑﻮد. از ﻃﺮﻓﯽ ﺗﻮﻟﯿﺪ ﮐﻨﻨﺪﮔﺎن ﭘﺮدازﻧﺪه ﺑﻪ ﮐﻤﮏ ﺑﺮﻧﺎﻣﻪ ﻧﻮﯾﺴﺎن ﭘﯽ ﺑﻪ ﺑﯿﮑﺎری زﯾﺎد ﭘﺮدازﺷﮕﺮﻫﺎ در زﻣﺎن ﺳﻮﯾﭻ ﮐﺮدن ﻓﺮاﯾﻨﺪ ﻫﺎ و ﻧﺦ ﻫﺎ ﺷﺪﻧﺪ ﮐﻪ ﺣﺪود ﻧﯿﻤﯽ از زﻣﺎن ﭘﺮدازش را ﺑﻪ ﻫﺪر ﻣﯽ داد. ﺑﺮاي ﺟﺒﺮان ﺣﺎﻓﻈﻪ ﮐﺶ را ﮔﺴﺘﺮش دادﻧﺪ اﻣﺎ ﺑﻪ دﻟﯿﻞ ﮔﺮان ﺑﻮدﻧﺶ ﺑﺎز دﭼﺎر ﻣﺤﺪودﯾﺖ ﺑﻮدﻧﺪ. ﺑﻨﺎﺑﺮاﯾﻦ ﭘﺮدازﻧﺪه ﻫﺎﯾﯽ ﺗﻮﻟﯿﺪ ﮐﺮدﻧﺪ ﮐﻪ ﺑﺘﻮاﻧﺪ ﭘﺮازش ﻣﻮازي را (در اﺑﺘﺪا) در دو ﻫﺴﺘﻪ ﺑﻪ اﺟﺮا ﺑﺮﺳﺎﻧﻨﺪ.

ﻧﺎم اﯾﻦ ﻫﺴﺘﻪ ﻫﺎ ﻫﺴﺘﻪ ﻫﺎی ﺳﺨﺖ اﻓﺰاری ﯾﺎ ﻓﯿﺰﯾﮑﯽ ﮔﺬاﺷﺘﻨﺪ. اﻧﺪك زﻣﺎﻧﯽ ﺑﻌﺪ ﻓﻨﺎوری ای ﺑﺮای رﺳﯿﺪن ﺑﻪ ﭘﺮدازش ﻣﻮازی اﻣﺎ در ﺳﻄﺢ ﻣﺤﺪود Hyper ﺗﺮی و ارزان ﺗﺮ ﺑﺎ ﻧﺎم اﺑﺮ ﻧﺨﯽ ﯾﺎ Hyper-Threading اراﺋﻪ ﮐﺮدﻧﺪ و ﻧﺎم آن را ﻫﺴﺘﻪ ﻫﺎی ﻣﻨﻄﻘﯽ ﯾﺎ ﻧﺦ ﻫﺎی ﺳﺨﺖ اﻓﺰاری ﮔﺬاﺷﺘﻨﺪ. ﺣﺎل ﻧﻮﺑﺖ ﺑﺮﻧﺎﻣﻪ ﻧﻮﯾﺴﺎن ﺑﻮد ﺗﺎ ﺑﺮﻧﺎﻣﻪ ﻫﺎی ﺑﺮاي اﺳﺘﻔﺎده از اﯾﻦ ﻓﻨﺎوری ﻫﺎی ﻧﻮﯾﻦ ﺑﻨﻮﯾﺴﻨﺪ. ﺑﺮﻧﺎﻣﻪ ﻧﻮﯾﺴﯽ ﻣﻮازی ﻋﻨﻮاﻧﯽ اﺳﺖ، ﮐﻪ ﻣﻮﺿﻮﻋﯽ ﮔﺴﺘﺮده در دﻧﯿﺎي ﻧﺮم اﻓﺰار اﯾﺠﺎد ﮐﺮده است.

روش موازی سازی(Parallelism(

ﺳﺎده ﺗﺮﯾﻦ ﺷﯿﻮه ﻣﻮازي ﺳﺎزي در ﻗﺎﻟﺐ Task ها ﺻﻮرت ﻣﯽ ﮔﯿﺮد، در ﻫﺮ Task ﺗﺎﺑﻊ ﯾﺎ ﻗﻄﻌﻪ ﮐﺪي ﻧﻮﺷﺘﻪ ﻣﯽ ﺷﻮد و ﺳﭙﺲ ﺑﻮﺳﯿﻠﻪ Delegate اي ﮐﻪ ﮐﺎر ﻣﺪﯾﺮﯾﺖ Task ﻫﺎ را ﺑﺮ ﻋﻬﺪه دارد اﯾﻦ  Task ﻫﺎ ﺑﺼﻮرت ﻣﻮازی ﺑﺴﺘﻪ ﺑﻪ ﻫﺴﺘﻪ ﻫﺎی ﻣﻨﻄﻘﯽ در دﺳﺘﺮس اﺟﺮا ﻣﯽ ﺷﻮﻧﺪ. روش ﻫﺎی ﺑﺴﯿﺎری ﺑﺮاي ﻣﻮازی ﺳﺎزی وﺟﻮد دارد ﻣﺎﻧﻨﺪ اﺳﺘﻔﺎده از ﮐﻼس Parallel.For یا Parallel.ForEach ﮐﻪ در ﺟﺎي ﺧﻮد ﮐﺎرﺑﺮد ﻫﺎی ﻣﺨﺘﺺ ﺑﻪ ﺧﻮدﺷﺎن را دارﻧﺪ. همیشه الگورﯾﺘﻢ ﻫﺎی ﺗﺮﺗﯿﺒﯽ را ﻧﻤﯽ ﺗﻮان ﺑﻪ الگورﯾﺘﻤﯽ ﻣﻮازي ﺗﺒﺪﯾﻞ ﮐﺮد ﭼﺮا ﮐﻪ ﮐﺪ ﻫﺎی ﺗﺮﺗﯿﺒﯽ ای ﻫﺴﺘﻨﺪ ﮐﻪ اﺟﺮاي ﮐﺪ ﻫﺎي دﯾﮕﺮ ﻧﯿﺎز ﺑﻪ ﺗﮑﻤﯿﻞ ﺷﺪن آن ﻫﺎ دارد. ﺑﺴﺘﻪ ﺑﻪ الگورﯾﺘﻢ درﺻﺪی از آن را ﻣﯽ ﺗﻮان ﻣﻮازی ﮐﺮد. ﻗﺒﻞ از ﻣﻮازی ﺳﺎزی ﺑﺎﯾﺪ ﻣﻮازی ﺳﺎزی را در ذﻫﻨﺘﺎن ﻃﺮاﺣﯽ ﮐﻨﯿﺪ.


Parallel Programming  یا برنامه نویسی موازی یعنی تقسیم یک مسئله به مسائل کوچکتر و سپردن آن ها به واحد های جداگانه برای پردازش کردن.این مسائل کوچک به صورت همزمان شروع به اجرا می کنند. Parallel Programming وظیفه یا Task را به اجزا مختلفی تقسیم می کند.

فرم های مختلفی از Parallel وجود دارد .مانند bit-level ،  instruction-level، data ، taskدر این آموزش راجع به Data Parallelism و Task Parallelism بحث خواهیم کرد.

تصور کنید هسته CPUمتشکل از چندین ریزپردازنده است که همه این ها به حافظه اصلی دسترسی دارند.هر کدام از این ریزپردازنده ها قسمتی از مسئله را حل می کنند.

Data Parallelism

این مورد بر روی توزیع دیتا در نقاط مختلف تمرکز می کند.یعنی داده را به بخش های مختلفی می شکند.هر کدام از این بخش ها به Thread جداگانه ای برای پردازش داده می شود.

Task Parallelism

این مفهوم وظایف یا Taskها را به بخش هایی شکسته و هر کدام را به یک Thread جهت پردازش می دهد.

در پروژه ای که به صورت ضمیمه این مقاله می باشد (در پروژه DataParallisem)به سه صورت مختلف وظیفه یا Task تعریف شده است.

1-به صورت Function

2- به صورت Delegate

3-به صورت لامبدا

کد این قسمت به صورت زیر می باشد

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
  
namespace TPL_part_1_creating_simple_tasks
{
    class Program
    {
        static void Main(string[] args)
        {
            //Action delegate
            Task task1 = new Task(new Action(HelloConsole));
 
            //anonymous function
            Task task2 = new Task(delegate
            {
                HelloConsole();
            });
             
            //lambda expression
                Task task3 = new Task(() = > HelloConsole());                 
             
            task1.Start();
            task2.Start();
            task3.Start();
             
            Console.WriteLine("Main method complete. Press any key to finish.");
            Console.ReadKey();
        }
        static void HelloConsole()
        {
            Console.WriteLine("Hello Task");
        }
    }
}

 

بعد از اجرا، هر کدام از Task ها اجرا شده البته به صورت همزمان و کارهای محوله به آنها را انجام میدهند.

آموزش موازی سازی در سی شارپ

 

در این آموزش بر روی مفهوم Data Parallelism تمرکز خواهیم کرد.توسط Data Parallelism عملیات یکسانی بر روی المانهای یک مجموعه یا آرایه به صورت همزمان انجام خواهد شد. که در فضای نام System.Threading.Tasks.Parallel قرار دارد. روش اصلی برای انجام Data Parallelismنوشتن یک تابع است که یک حلقه ساده بدون Thread دارد .

public static void DataOperationWithForeachLoop()
       {
           var mySource = Enumerable.Range(0, 1000).ToList();
           foreach (var item in mySource)
           {
               Console.WriteLine("Square root of {0} is {1}", item, item * item);
           }
       }

 

خروجی برنامه را در زیر می بینید

آموزش موازی سازی در سی شارپ

 

عملیات Data Parallelism را می توان با یک حلقه foreach موازی هم انجام داد.

public static void DataOperationWithDataParallelism()
        {
            var mySource = Enumerable.Range(0, 1000).ToList();
            Parallel.ForEach(mySource, values = > CalculateMyOperation(values));
        }
 
        public static  void CalculateMyOperation(int values)
        {
            Console.WriteLine("Square root of {0} is {1}", values, values * values);
        }

بعد از اجرا شکل زیر را خواهید دید.

آموزش موازی سازی در سی شارپ

 

در این کد در داخل حلقه Foreach  یک تابع Delegate قرار دادیم در این تابع به ازای هر تکرار حلقه بر روی مجموعه تابعی که درون Delegate فراخوانی کرده ایم اجرا خواهد شد.

Data Parallelism توسط PLINQ

PLINQ به معنای Parallel LINQ است .این نسخه از لینک جهت پیاده سازی لینک بر روی پردازنده های چند هسته ای نوشته شده است.

توسط لینک می توان اطلاعات را از چندین منبع بازیابی کرد.و در نهایت این نتایج با هم ترکیب می شوند تا نتیجه نهایی Query به دست آید.اما اگر از PLINQاستفاده کنیم این دستورات به جای اینکه پشت سر هم اجرا شوند به صورت موازی اجرا می شوند.

برای این که از PLINQ استفاده کرد فقط کافی است که در انتهای عبارت لینک از AsParallel استفاده کنیم.به کد زیر توجه کنید

public static void DataOperationByPLINQ()
    {
        long mySum = Enumerable.Range(1, 10000).AsParallel().Sum();
        Console.WriteLine("Total: {0}", mySum);
    }

بعد از اجرا شکل زیر را خواهید دید

آموزش موازی سازی در سی شارپ

 

برای به دست آوردن اعداد فرد در این مجموعه توسط Plinq از کد زیر استفاده می کنیم

public static void ShowEvenNumbersByPLINQ()
      {
          var numers = Enumerable.Range(1, 10000);
          var evenNums = from number in numers.AsParallel()
                         where number % 2 == 0
                         select number;
 
          Console.WriteLine("Even Counts :{0} :", evenNums.Count());
      }

پس از اجرا شکل زیر را خواهید دید

آموزش موازی سازی در سی شارپ

 

توسط متد Parallel.Invoke() می توانید چندین متد را مانند شکل زیر به صورت همزمان اجرا کنید.

Parallel.Invoke(    
() = > Method1(mycollection),    
() = > Method2(myCollection1, MyCollection2),    
() = > Method3(mycollection));

 MaxDegreeOfParallelism

ماکزیمم تعداد پردازش های موازی را مشخص می کند در کد زیر و در داخل Foreach در پارامتر دوم ماکزیمم تعداد پردازش های موازی مشخص شده اشت.

loopState.Break()

توسط این کد به Thread هایی که پردازش آنها طول کشیده اجازه می دهیم که بعدا Break شوند.به کد زیر توجه کنید.

var mySource = Enumerable.Range(0, 1000).ToList();    
int data = 0;    
Parallel.ForEach(    
    mySource,    
    (i, state) = >    
    {    
        data += i;    
        if (data  >  100)    
        {    
            state.Break();    
            Console.WriteLine("Break called iteration {0}. data = {1} ", i, data);    
        }    
    });    
Console.WriteLine("Break called data = {0} ", data);    
Console.ReadKey();

منبع

 


فایل ضمیمه این آموزش

TPL_part_1_creating_simple_tasks

رمز فایل: behsanandish.com


دانلود کتاب آموزش برنامه نویسی موازی با #C

Parallel Programming In Csharp

رمز فایل: behsanandish.com

 

استفاده از متد 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 ای داریم که در حال پردازش 1000 فایل است و کاربر باید این امکان را داشته باشد که Task در حال اجرا را متوقف کند. عملیات متوقف کردن Task ها هم برای متدهای کلاس Parallel امکان پذیر است و هم کلاس Task. برای اینکار می بایست از کلاس CancellationToken استفاده کنیم. برای مثال Task زیر را در نظر بگیرید که حاصل میانگین جمع اعداد 1 تا 100 را محاسبه می کند:

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 بود پیغامی را نمایش بده و مقدار 0 را برگردان. کد نهایی ما به صورت زیر می باشد:

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!
0
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، نیاز به حجم زیادی از کدها دارد.

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

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

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

برای آشنایی بیشتر برنامه ای از نوع Windows Forms Application ایجاد کرده، یک Button بر روی فرم قرار می دهیم. زمانی که بر روی Button ایجاد شده کلیک می شود، یک متد دیگر فراخوانی شده و بعد از یک وقفه 10 ثانیه ای عبارتی را بر میگرداند و در نهایت این متن به عنوان 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 ایجاد شده، 10 ثانیه باید منتظر شده تا عنوان فرم تغییر کند. اما با انجام یکسری تغییرات در کد بالا، می توان بوسیله کلمات کلیدی 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 به مدت 10 ثانیه متوقف می شود و بعد از 10 ثانیه یک رشته به عنوان خروجی برگردانده می شود. البته این رشته تحت یک شئ از نوع 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 ها در دات نت آشنا شدیم. از نسخه 4 دات نت قابلیتی اضافه شد با نام 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 اعضای لیست را پردازش می کنیم، با هر بار اجرا خروجی های متفاوتی دریافت خواهیم کرد:

8
5
Thread Id: 6
4
2
Thread Id: 1
6
3
Thread Id: 5
9
Thread Id: 5
10
Thread Id: 5
1
Thread Id: 5
Thread Id: 4
Thread Id: 6
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);
});

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

1
Thread Id: 1
9
3
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 به صورت همزمان در سی شارپ

کار با Thread ها در زبان سی شارپ :: آشنایی با CLR ThreadPool در دات نت

به عنوان آخرین مبحث از سری مباحث مرتبط کار با Thread ها به سراغ نقش CLR ThreadPool می رویم. در قسمت ماهیت Asynchronous در delegate ها گفتیم که بوسیله متد BeginInvoke و EndInvoke می توان یک متد را به صورت Asynchronous فراخوانی کرد، اما نکته ای که اینجا وجود دارد این است که CLR با این کار به صورت مستقیم Thread جدیدی ایجاد نمی کند!

برای کارآیی بیشتر متد BeginInvoke یک آیتم در ThreadPool ایجاد می کند که فراخوانی آن در زمان اجرا مدیریت می شود. برای اینکه بتوانیم به طور مستقیم با این قابلیت در ارتباط باشیم، کلاسی با نام ThreadPool در فضای نام System.Threading وجود دارد که قابلیت این کار را به ما می دهد.

برای مثال اگر بخواهیم فراخوانی یک متد را به Thread Pool بسپاریم، کافیست از متد استاتیکی که در کلاس ThreadPool و با نام QueueUserWorkItem وجود دارد استفاده کنیم. بوسیله این متد می توان Callback مرتبط با کار مد نظر و همچنین شئ ای که به عنوان state استفاده می شود را به این متد ارسال کنیم. در زیر ساختار این کلاس را مشاهده می کنید:

public static class ThreadPool
{
    public static bool QueueUserWorkItem(WaitCallback callback);
    public static bool QueueUserWorkItem(WaitCallback callback, object state);
} 

پارامتر WaitCallBack می تواند به هر متدی که نوع بازگشتی آن void و پارامتر ورودی آن از نوع System.Object است اشاره کند. دقت کنید که اگر مقداری برای state مشخص نکنید، به صورت خودکار مقدار null به آن پاس داده می شود. برای آشنایی بیشتر با این کلاس به مثال زیر که همان مثال نمایش اعداد در خروجی است دقت کنید، در ابتدا کلاس Printer:

public class Printer
{
    object threadLock = new object();
    public void PrintNumbers()
    {
        lock (threadLock)
        {
            Console.Write("{0} is printing numbers &gt; ", Thread.CurrentThread.Name);
            for (int counter = 0; counter &lt; 10; counter++)
            {
                Thread.Sleep(200 * new Random().Next(5));
                Console.Write("{0},", counter);
            }
            Console.WriteLine();
        }
    }
}

در ادامه کد متد Main که با استفاده از ThreadPool عملیات ایجاد Thread ها را انجام می دهد:

class Program
{
    static void Main(string[] args)
    {
        var printer = new Printer();
        for (int index = 0; index &lt; 10; index++)
            ThreadPool.QueueUserWorkItem(PrintNumbers, printer);
 
        Console.ReadLine();
    }
 
    static void PrintNumbers(object state)
    {
        var printer = (Printer) state;
        printer.PrintNumbers();
    }
}

شاید این سوال برای شما پیش بیاید که مزیت استفاده از ThreadPool نسبت به اینکه به صورت دستی عملیات ایجاد و فراخوانی thread ها را انجام دهیم چیست؟ در زیر به برخی از مزایای اینکار اشاره می کنیم:

  1. Thread Pool به صورت بهینه تعداد عملیات مدیریت thread هایی که می بایست ایجاد شوند، شروع بشوند یا متوقف شوند را برای ما انجام می دهد.
  2. با استفاده از Thread Pool شما می توانید تمرکز خود را به جای ایجاد و مدیریت Thread ها بر روی منطق و اصل برنامه بگذارید و سایر کارها را به عهده CLR بگذارید.

اما موارد زیر نیز را مد نظر داشته باشید که مزیت ایجاد و مدیریت thread ها به صورت دستی می باشند:

  1. thread های ایجاد شده توسط thread pool به صورت پیش فرض از نوع foreground هستند، همچنین شما می توانید بوسیله ایجاد thread ها به صورت دستی Priority آن ها را نیز مشخص کنید.
  2. اگر ترتیب اجرای thread ها برای شما مهم باشند یا نیاز داشته باشید thread ها را به صورت دستی حذف یا متوقف کنید این کار بوسیله thread pool امکان پذیر نیست.

با به پایان رسیدن این مطلب، بحث ما بر روی Thread ها به پایان می رسد. به امید خدا در مطالب بعدی راجع به بحث Parallel Programming صحبت خواهیم خواهیم کرد.

 

منبع


قسمت اول آموزش-برنامه نویسی 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 به صورت همزمان در سی شارپ