نوشته‌ها

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

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

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

Process چیست؟

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

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

 

parallel programming

 

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

parallel programming

هر یک Process های در حال اجرا حافظه، منابع تخصیص داده شده و روند اجرای مربوط به خود را دارند. در تصویر بالا نیز مشخص است، برای مثال در زمان گرفتن عکس بالا، Prcess مربوط به برنامه paint.net مقدار ۲۰٫۴% از CPU و همچنین ۱۰۷٫۶MB از حافظه را اشغال کرده است. در اینجا بیشتر به بحث 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);
    }
}

همانطور که مشاهده می کنید در کد بالا ۳ شئ از نوع 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 ها در زبان سی شارپ :: آشنایی با مشکل Concurrency در برنامه های Multi-Threaded و راهکار های رفع این مشکل

زمانی که ما برنامه های Multi-Threaded می نویسیم، برخی اوقات Thread های ایجاد شده به داده های مشترک در سطح برنامه دسترسی دارند و وظیفه ما به عنوان برنامه نویس این است که مطمئن باشیم دسترسی چند Thread به داده های مشترک باعث بروز مشکل نمی شود. برای آشنایی بیشتر با این موضوع شرایطی را در نظر بگیرید که یک متد قرار است در چندین thread مختلف به صورت جداگانه اجرا شود، بعد از شروع کار هر thread زمانبندی اجرا توسط CLR به هر thread به صورت خودکار انجام شده و ما نمی توانیم دخالتی در این موضوع داشته باشیم، ممکن است در این بین اختصاص زمان به یک thread بیش از thread دیگر انجام شود و در این بین خروجی مناسب مد نظر ما ایجاد نمی شود. برای آشنایی با این موضوع متد PrintNumbers که در زیر تعریف کردیم را در نظر بگیرید:

public static void PrintNumbers()
{
    Console.Write("{0} is printing numbers  >  " , Thread.CurrentThread.Name);
    for (int counter = 0; counter  <  10 ;  counter++)
    {
        Thread.Sleep(200*new Random().Next(5));
        Console.Write("{0},", counter);
    }
    Console.WriteLine();
}

در مرحله بعد متد Main را به صورت زیر تغییر می دهیم تا ۱۰ thread ایجاد شده و سپس کلیه thread ها اجرا شوند:

Thread[] threads = new Thread[10];

for (int index = 0; index  <  10 ;  index++)
{
    threads[index] = new Thread(PrintNumbers);
    threads[index].Name = string.Format("Worker thread #{0}.", index);
}

foreach (var thread in threads)
{
    thread.Start();
}

Console.ReadLine();

همانطور که مشاهده می کنید کلیه thread ها به صورت همزمان اجرا می شوند، اما پس از اجرا کد بالا، خروجی برای بار اول به صورت خواهد بود، البته دقت کنید که با هر بار اجرا خروجی تغییر می کند و ممکن است برای بار اول خروجی زیر برای شما تولید نشود:

Worker thread #0. is printing numbers  >  Worker thread #1. is printing numbers  >  Worker thread #2. is printing numbers  >  Worker thread #3. is printing numbers  >  0,Worker thread #4. is printing numbers  >  0,1,1,2,3,2,Worker thread #5. is printing numbers  >  0,4,3,1,4,2,5,Worker thread #6. is printing numbers  >  5,3,Worker thread #7. is printing numbers  >  4,6,6,0,Worker thread #8. is printing numbers  >  Worker thread #9. is printing numbers  >  0,0,7,5,7,0,1,0,1,0,6,8,8,1,2,1,1,0,2,9,
۷,۳,۲,۲,۹,
۲,۱,۸,۳,۳,۳,۴,۲,۱,۹,
۴,۴,۴,۵,۳,۳,۵,۵,۵,۶,۲,۶,۶,۷,۶,۴,۴,۷,۷,۸,۹,
۸,۹,
۷,۸,۹,
۵,۵,۳,۸,۶,۷,۸,۹,
۶,۷,۸,۹,
۴,۵,۹,
۶,۷,۸,۹,

اگر برنامه را مجدد اجرا کنید خروجی متفاوتی از خروجی قبلی دریافت خواهیم کرد:

Worker thread #0. is printing numbers  >  Worker thread #1. is printing numbers  >  Worker thread #2. is printing numbers  >  Worker thread #3. is printing numbers  >  Worker thread #4. is printing numbers  >  Worker thread #5. is printing numbers  >  Worker thread #6. is printing numbers  >  0,Worker thread #7. is printing numbers  >  1,2,0,1,3,2,4,3,Worker thread #8. is printing numbers  >  Worker thread #9. is printing numbers  >  0,0,0,0,0,0,5,4,5,6,7,8,9,
۶,۷,۸,۹,
۱,۱,۰,۰,۱,۱,۱,۱,۱,۱,۲,۲,۲,۲,۲,۲,۲,۲,۳,۳,۳,۴,۵,۶,۷,۸,۹,
۳,۳,۳,۳,۳,۴,۴,۴,۴,۵,۶,۷,۸,۹,
۵,۶,۷,۸,۹,
۵,۶,۷,۵,۶,۷,۸,۹,
۸,۴,۴,۴,۵,۵,۶,۶,۷,۷,۹,
۵,۸,۸,۶,۹,
۹,
۷,۸,۹,

همانطور که مشاهده می کنید خروجی های ایجاد کاملاً با یکدیگر متفاوت هستند. مشخص است که در اینجا مشکلی وجود دارد و همانطور که در ابتدا گفتیم این مشکل وجود همزمانی یا Concurrency در زمان اجرای thread هاست. زمابندی CPU برای اجرای thread ها متفاوت است و با هر بار اجرا زمان های متفاوتی به thread ها برای اجرا تخصیص داده می شود. اما خوشبختانه مکانیزم های مختلفی برای رفع این مشکل و پیاده سازی Synchronizqation وجوددارد که در ادامه به بررسی راهکاری های مختلف برای حل مشکل همزمانی می پردازیم.

پیاده سازی Synchronization با کلمه کلیدی lock

اولین مکانیزم مدیریت همزمانی در زمان اجرای Thread ها استفاده از کلمه کلیدی lock است. این کلمه کلیدی به شما این اجازه را می دهد تا یک scope مشخص کنید که این scope باید به صورت synchronized بین thread ها به اشتراک گذاشته شود، یعنی زمانی که یک thread وارد scope ای شد که با کلمه کلیدی lock مشخص شده، thread های دیگر باید منتظر شوند تا thread جاری که در scope قرار دارد از آن خارج شود. برای استفاده از lock شما اصطلاحاً می بایست یک token را برای scope مشخص کنید که معمولاً این کار با ایجاد یک شئ از نوع object و مشخص کردن آن به عنوان token برای synchronization استفاده می شود. شیوه کلی استفاده از lock به صورت زیر است:

lock(token)
{
    // all code in this scope are thread-safe
}

اصطلاحاً می گویند کلیه کدهایی که در بدنه lock قرار دارند thread-safe هستند. برای اینکه کد داخل متد PrintNumbers به صورت thread-safe اجرا شود، ابتدا باید یک شئ برای استفاده به عنوان token در کلاس Program تعریف کنیم:

class Program
{
    public static object threadLock = new object();

    ....

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

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

با اعمال تغییر بالا، زمانی که thread جدیدی قصد وارد شدن به scope مشخص شده را داشته باشد، باید منتظر بماند تا کار thread جاری به اتمام برسد تا اجرای thread جدید شروع شود. با اعمال تغییر بالا، هر چند بار که کد نوشته شده را اجرا کنید خروجی زیر را دریافت خواهید کرد:

Worker thread #0. is printing numbers  >  0,1,2,3,4,5,6,7,8,9,
Worker thread #1. is printing numbers  >  0,1,2,3,4,5,6,7,8,9,
Worker thread #2. is printing numbers  >  0,1,2,3,4,5,6,7,8,9,
Worker thread #3. is printing numbers  >  0,1,2,3,4,5,6,7,8,9,
Worker thread #4. is printing numbers  >  0,1,2,3,4,5,6,7,8,9,
Worker thread #5. is printing numbers  >  0,1,2,3,4,5,6,7,8,9,
Worker thread #6. is printing numbers  >  0,1,2,3,4,5,6,7,8,9,
Worker thread #7. is printing numbers  >  0,1,2,3,4,5,6,7,8,9,
Worker thread #8. is printing numbers  >  0,1,2,3,4,5,6,7,8,9,
Worker thread #9. is printing numbers  >  0,1,2,3,4,5,6,7,8,9,

پیاده سازی Synchronization بوسیله کلاس Monitor

در قسمت قبل که از کلمه کلیدی lock استفاده کردیم، در حقیقت به در پشت زمینه از کلاس Monitor که در فضای نام System.Threading قرار دارد استفاده شده است. زمانی که از کلمه کلیدی lock استفاده می کنیم این کد تبدیل به کدی می شود که از Monitor برای پیاده سازی Synchronization استفاده می کند (می توان این موضوع را با ابزار ildasm.exe و مشاهده کد IL متوجه شد). نحوه استفاده از کلاس Mutex را در کد زیر که تغییر داده شده متد PrintNumbers است مشاهده می کنید:

public static void PrintNumbers()
{
    Monitor.Enter(threadLock);            
    try
    {
        Console.Write("{0} is printing numbers  >  " , Thread.CurrentThread.Name);
        for (int counter = 0; counter  <  10 ; counter++)
        {
            Thread.Sleep(200*new Random().Next(5));
            Console.Write("{0},", counter);
        }
        Console.WriteLine();
    }
    finally
    {
        Monitor.Exit(threadLock);
    }
}

متد Enter در کلاس Monitor اعلام می کند که thread ای وارد محدوده ای شده است که باید thread-safe باشد. به عنوان پارامتر ورودی برای این متد token ایجاد شده یعنی obj را ارسال می کنیم. در قدم بعدی کل کدی که مربوط به ناحیه thread-safe است باید داخل بدنه try-catch نوشته شود و البته بخش finaly نیز برای آن نوشته شده باشد، همانطور که می دانید بخش finally قسمتی از بدنه try است که در هر صورت اجرا می شود. در قسمت finally متد Exit را با پارامتر threadLock که همان token مربوطه است فراخوانی می کنیم، یعنی thread در حال اجرا از محدوده thread-safe خارج شده است. دلیل نوشتن try-catch در کد بالا این است که در صورت وقوع خطا عملیات خروج از محدوده thread-safe در هر صورت انجام شود و thread های دیگر منتظر ورود به این محدوده نمانند. شاید این سوال برای شما بوجود بیاید که با وجود کلمه کلیدی lock چه دلیلی برای استفاده از کلاس Monitor وجود دارد؟ دلیل این موضوع کنترل بیشتر بر روی ورود thread ها و اجرای کدهای thread-safe است. برای مثال، بوسیله متد Wait در کلاس Monitor می توان مشخص کرد که یک thread چه مدت زمانی را برای ورود به ناحیه thread-safe باید منتظر بماند و همچنین متدهای Pulse و PulseAll را می توان برای اطلاع رسانی به سایر thread ها در مورد اینکه کار thread جاری به اتمام رسیده استفاده کرد. البته در اکثر موقعیت ها استفاده از کلمه کلیدی lock کافی است و نیازی به استفاده از کلاس Monitor نمی باشد.

پیاده سازی Synchronization با استفاده از کلاس Interlocked

زمانی که قصد داریم مقدار یک متغیر را تغییر دهیم یا عملگرهای ریاضی را بر روی دو متغیر اعمال کنیم، عملیات های انجام شده به دو صورت Atomic و Non-Atomic انجام می شوند. مبحث عملیات عملیات های Atomic چیزی بیش از چند خط نیاز دارد تا توضیح داده شود، اما به طور خلاصه می توان گفت که عملیات های Atomic عملیات هایی هستند که تنها در یک مرحله یا step انجام می شوند. اگر کدهای IL مرتبط به مقدار دهی متغیرها و البته عملگرهای ریاضی را بر روی بیشتر نوع های داده در دات نت مشاهده کنیم میبینیم که این عملیات ها بیشتر به صورت non-atomic هستند، یعنی بیش از یک مرحله برای انجام عملیات مربوط نیاز است که این موضوع می تواند در برنامه های Multi-threaded مشکل ساز شود. در حالت ساده می توان بوسیله مکانیزم lock عملیات synchronization را برای این عملیات ها پیاده سازی کرد:

int value = 1;
lock(token)
{
    value++;
}

اما برای شرایطی مانند مثال بالا استفاده از lock یا کلاس monitor باعث ایجاد overhead اضافی می شود که برای حل این مشکل می توان از کلاس Interlocked برای اعمال synchronization در اعمال انتساب مقدار یا مقایسه مقادیر استفاده کرد. برای مثال، زمانی که می خواهیم مقدار یک متغیر را با استفاده از کلاس Interlocked اضافه کنیم به صورت می توانیم این کار را پیاده سازی کنیم:

int myNumber = 10;
Interlocked.Increment(ref myNumber);

متد Increment در کلاس Interlocked یک مقدار به متغیر مشخص شده اضافه می کند و البته این کار در محیط Thread-Safe انجام می شود. برای کاهش مقدار می توان از متد Decrement به صورت مشابه استفاده کرد:

int myNumber = 10;
Interlocked.Decrement(ref myNumber);

همچنین برای مقایسه و مقدار دهی مقدار یک متغیر می توان از متد CompareExchange استفاده به صورت زیر استفاده کرد:

int myNumber = 10;
Interlocked.CompareExchange(ref myNumber, 15, 10);

در کد بالا در صورتی که مقدار متغیر myNumber برابر ۱۰ باشد، مقدار آن با ۱۵ عوض خواهد شد.

پیاده سازی Synchronization بوسیله خاصیت [Synchronization]

می دانیم که خاصیت [Synchronization] زمانی که بر روی یک کلاس قرار میگیرد، باعث می شود که کد داخل آن کلاس به صورت Thread-Safe اجرا شود. در این قسمت می خواهیم کد مربوط به متد PrintNumbers را به صورت Thread-Safe و با کمک Object Context Boundry و همچنین خاصیت [Synchronization] پیاده سازی کنیم. برای این کار ابتدا یک کلاس با نام Printer پیاده سازی کرده و متد PrintNumbers را داخل آن قرار می دهیم. دقت کنید که کلاس Printer می بایست از کلاس ContextBoundObject مشتق شده باشد و خاصیت [Synchronization] بر روی آن قرار گرفته باشد:

[Synchronization]
public class Printer : ContextBoundObject
{
    public void PrintNumbers()
    {
        Console.Write("{0} is printing numbers  >  " , Thread.CurrentThread.Name);
        for (int counter = 0; counter  <  10 ; counter++)
        {
            Thread.Sleep(200*new Random().Next(5));
            Console.Write("{0},", counter);
        }
        Console.WriteLine();
    }
}

پس از انجام تغییرات بالا متد Main را به صورتی تغییر می دهیم که از متد Print در کلاس Printer استفاده کند:

Printer printer = new Printer();

Thread[] threads = new Thread[10];

for (int index = 0; index  <  10 ; index++)
{
    threads[index] = new Thread(printer.PrintNumbers);
    threads[index].Name = string.Format("Worker thread #{0}.", index);
}

foreach (var thread in threads)
{
    thread.Start();
}

Console.ReadLine();

با انجام کارهای بالا و اجرای برنامه مشاهده خواهیم کرد که با وجود عدم استفاده از کلمه کلیدی lock یا کلاس Monitor، برنامه به صورت Thread-Safe اجرا شده و خروجی مناسب برای ما تولید می شود. در اینجا مبحث مربوط به Synchronization به اتمام رسیده و در قسمت در مورد کلاس Timer در فضای نام 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 به صورت همزمان در سی شارپ