استفاده از Multi Threading در C#.NET به همراه مثال

در تاریخ ۱۳۹۴/۲/۱۳ ساعت ۲۲:۵۱:۲۶
عباسعلی خالصی

مقدمه

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

حال که با مقدماتی از Multi Threading آشنا شده ایم میخواهیم با استفاده از دو روش(معمولی و چند نخی) مزیت Multi Threading را نسبت به روش های معمولی مورد بررسی قرار دهیم. یک برنامه عادی نوشته شده به صورت همزمان با دیگر برنامه های ویندوز اجرا می شود، آیا این همزمانی در برنامه هم لحاظ می شود؟ بگذارید این مسئله را با استفاده از دو Timer در برنامه مورد بررسی قرار دهیم. فرض کنید در برنامه شما دو عدد Timer وجود دارد که در بازه زمانی یک میلی ثانیه، قطعه کدی را اجرا می کند. به عنوان مثال کدهای مربوط به دو Timer به صورت زیر است:

:Timer1

private void timer1_Tick(object sender, EventArgs e)
{
    this.Left++;
}

:Timer2

private void timer2_Tick(object sender, EventArgs e)
{
    for (long i = 0; i < 100000000000; i++)
    {
        //Do work Or Operation Code
    }
}

در این برنامه انتظار داریم هر دو تایمر مجزای از هم کار کنند. پس تایمر یک هر یک میلی ثانیه کار خود را انجام داده، فرم جاری را یک واحد به راست حرکت دهد و تایمر 2 نیز حلقه خود را به صورت مجزا اجرا کند. ولی این امر اتفاق نمی افتد و این دو قطعه کد به صورت همزمان اجرا نخواهند شد(می توانید امتحان کنید). وقتی CPU وارد اجرای دستورات Timer دو شود، در داخل حلقه تکرار آن مانده و تا زمان پایان رسیدن آن به سمت اجرای Timer یک نخواهد رفت، ممکن است این شمارش چندین میلی ثانیه طول بکشد. برای حل این مشکل(اجرای همزمان فرآیندها) بحث Threading در C#.NET مطرح شده است. در ادامه به توضیح در خصوص پردازش موازی فرآیندها و به دنبال آن به پیاده سازی چند مثال ساده می پردازیم.

پردازش موازی فرآیندها، چند نخی(Multi Threading)

می دانیم که مهمترین مزایای  Threading اجرای همزمان فرآیند ها است. برای پیاده سازی این نوع برنامه نویسی ابتدا می بایست فضای کاری System.Threading را به پروژه خود اضافه نماییم. در این فضای کاری کلاس ها و interface هایی وجود دارندکه دسترسی به برنامه نویسی چند نخی(Multi Thread) را ممکن می کند. پر کاربرد ترین کلاس های موجود در فضای کاری ذکر شده شامل موارد زیر است:

-  InterLock: با استفاده از این کلاس و اعضای آن می توان قسمتی از برنامه را قفل گذاشته و اجرای آن را موقتاً متوقف کرد.

-  Monitor: با استفاده از این کلاس و اعضای آن می توان الگوریتم مانیتور را برای اجرای فرآیندها پیاده سازی نمود.(این الگوریتم در مباحث سیستم عامل مطرح است)

-  Mutex: نوعی دیگر از الگوریتم پیاده سازی اجرای موازی فرآیند ها است.

-  ReaderWriterLock: الگوریتم نویسنده و خواننده ی سیستم عامل ها را می توان توسط این کلاس و اعضای آن پیاده سازی کرد.

-  Semaphore: برای پیاده سازی الگوریتم Semaphore از این کلاس استفاده می شود.

-  Thread: مهم ترین کلاس برای اجرای موازی فرآند ها است که در ادامه به شرح آن می پردازیم.

-  کلاس شمارشی ThreadPriority: با استفاده از این نوع شمارشی می توان اجرای فرآیندها را اولویت بندی کرد.

-  و ...

استفاده از کلاس Thread

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

public sealed class Thread : CriticalFinalizerObject, _Thread

برنامه ای که توسط برنامه نویس نوشته می شود به صورت یک Process در نظر گرفته می شود. هر کدام از این فرآیند ها می تواند تک جزیی(Single Threading) یا چند جزیی(Multi Threading) باشند. می توانیم بگوییم که یک متد یا زیر برنامه می تواند معادل با یک Thread باشد. کلاس Thread هم دارای اعضای استاتیک و هم غیر استاتیک است. مهم ترین متدهای این کلاس شامل موارد زیر است:

-  ()Start: برای شروع Thread استفاده می شود.

-  Sleep(): این متد استاتیک برای معلق(Suspend) کردن یک فرآیند در بازه زمانی مشخص بر حسب میلی ثانیه استفاده شده است.(البته پس از سپری شدن این بازه فرآیند دوباره شروع به کار می کند)

-  ()Abort: با استفاده از این متد می توان Thread را از بین برده و یا جلوی فعالیت آن را گرفت.

()Suspend: یک فرآیند را معلق کرده و تازمانی که متد ()Resumeفراخوانی نشود در همین حالت باقی می ماند.

-  ()Resume: با این متد می توان نخ معلق شده را دوباره راه اندازی کرد.

()Join: وقتی از متد ()Join برای یک Thread استفاده می کنیم تا زمانی که آن Thread تمام(Terminate) نشود، نخ های بعدی متوقف خواهند ماند و بلافاصله بعد از اتمام Thread مورد نظر، باقی فرآیندها به صورت موازی اجرا می شوند.(البته دقت شود که متد ()Join می بایست بعد از متد Start فراخوانی شود.)

*نکته:

برنامه ها می توانند توسط برنامه نویس هم در محیط Win Form و هم در محیطConsole پیاده سازی شوند، برنامه های نوشته شده در محیط Console بدون هیچ تغییری می توانند اجرا شوند ولی در محیط Form باید حتماً دستور زیر را در سازنده فرم نوشت:

using System.Threading;

که برخی استفاده از روش فوق را بنا به دلایلی توصیه نمی کنند و روش هایی نظیر استفاده از Delegate  ها را پیشنهاد می کنند، بررسی Delegate  ها دراین مقوله نمی گنجد، لذا برای یادگیری استفاده از Delegate ها می توانید به ویدئوی آموزشی مراجعه نمایید.

پیاده سازی چند نخی در C#.NET با استفاده ازکلاس Thread

حال که مقدمات استفاده از Thread را فراگرفتیم به سراغ پیاده سازی پروژه میرویم. برای پیاده سازی ابتدا یک پروژه به زبان C# در Visual Studio از نوع Console  ایجاد کرده و درون فایل program.cs مراحل زیر را طی می کنیم:

مثال1: می خواهیم برنامه ای بنویسیم که با استفاده از Thread اعداد 1 تا 20 را به صورت نامرتب و ترکیبی نمایش دهد.(لازم به ذکر است برای این کار دو متد مجزا به نام های Thread1 و Thread2 پیاده سازی کرده و درون هر کدام دستورات مربوطه را می نویسیم و برای ایجاد وقفه از متد ()Sleep برای هر کدام استفاده می کنیم.)

1-  

using System.Threading;

اضافه کردن فضای کاری System.Threading به فایل program.cs

2-  نوشتن دو متد به نام های Thread1 و Thread2 که هر کدام عملیاتی مجزا منتها شبیه به هم انجام میدهند. توجه داشته باشید که متد ()Sleep از نوع استاتیک است. اگر مقدار پارامتر عددی که بر حسب میلی ثانیه است را در هر یک از ()Sleepها زیاد شود، آن Thread کار خود را دیرتر به اتمام خواهد رسانید(می توانید با مقدار دهی به هر کدام امتحان کنید).

public static void Thread1()
{
    for (int i = 0; i <= 10; i++)
    {
        Console.WriteLine("T1 {0}", i);
        Thread.Sleep(0);
    }
}




public static void Thread2()
{
    for (int i = 11; i < 20; i++)
    {
        Console.WriteLine("T2 {0}", i);
        Thread.Sleep(0);
    }
{

3-  عملیات وهله سازی کلاس Thread و ThreadStart و قرار دادن نام هر یک از متد های ایجاد شده در سازنده کلاس ThreadStart و پس از آن هر یک از نخ ها توسط متد Start شروع به کار می کنند.

4-  

static void Main(string[] args)
{
   Thread t1 = new Thread(new ThreadStart(Thread1));
   Thread t2 = new Thread(new ThreadStart(Thread2));
  
   t1.Start();
   t2.Start();
}

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

T1: 0 T1: 1 T1: 2 T1: 3 T1: 4 T1: 5 T2: 11 T2: 12 T2: 13 T2: 14 T2: 15 T2: 16 T1: 6 T1: 7 T2: 17 T1: 8 T1: 9 T1: 10 T2: 18 T2: 19 

مثال2: می خواهیم برنامه ای بنویسیم که با استفاده از کلاس Thread و متد ()Join، ابتدا اعداد 1 تا 5 را به صورت مرتب و سپس اعداد 6 تا 15 را به صورت نامرتب و تودرتو تولید و در خروجی چاپ کند.(لازم به ذکر است برای این کار سه متد مجزا به نام های Thread1 و Thread2 و 3Thread پیاده سازی کرده و درون هر کدام دستورات مربوطه را می نویسیم.)

*نکته: بر خلاف مثال قبل که از کلاس ThreadStart کمک می گرفتیم، در اینجا از این کلاس استفاده نخواهیم کرد و کاربرد متد ()Join را معرفی می کنیم.

1-  نوشتن سه متد به نام های Thread1 و Thread2 و Thread3 که هر کدام عملیاتی مجزا انجام میدهند. متد Thread1 موظف است اعداد یک تا 10 را تولید و در خروجی چاپ نماید. Thread2 موظف است اعداد 11 تا 20 را تولید و در خروجی چاپ نماید. Thread3 موظف است اعداد 21 تا 30 را تولید و در خروجی چاپ نماید، این مثال بدین صورت است که اگر هر سه متد را در حال عادی پشت سر هم اجرا کنید خروجی مرتب دارد ولی در قسمت بعد با استفاده از متد ()Join دیگر این چنین نخواهد بود.

public static void Thread1()
{
    for (int i = 0; i <= 10; i++)
    {
        Console.WriteLine("T1: {0}", i);
        Thread.Sleep(0);
    }
}




public static void Thread2()
{
    for (int i = 11; i <= 20; i++)
    {
        Console.WriteLine("T2: {0}", i);
        Thread.Sleep(0);
    }
}
public static void Thread3()
{
    for (int i = 21; i <= 30; i++)
    {
        Console.WriteLine("T3: {0}", i);
        Thread.Sleep(0);
    }
}

2-  در این مرحله عملیات وهله سازی کلاس Thread و قرار دادن هر متد درون سازنده کلاس Thread و پس از آن هر یک از نخ ها توسط متد Start شروع به کار می کنند. در این مثال پس از شروع نخ اول از متد ()Join برای نخ اول استفاده کرده ایم، این بدین خاطر است که بگوییم نخ اول را به اتمام برسان و پس از آن نخ های بعدی را به صورت ترکیبی اجرا کن، خروجی این مثال را می توانید در مرحله بعد ملاحظه کنید.

static void Main(string[] args)
{
    Thread t1 = new Thread(Thread1);
    Thread t2 = new Thread(Thread2);
    Thread t3 = new Thread(Thread3);
    t1.Start();
    t1.Join();
    
    t2.Start();
    t3.Start();
    Console.ReadLine();
}

3-  خروجی دستورات بالا شبیه به خروجی زیر می شود. خروجی روبرو نشان دهنده این است که ابتدا نخ اول اجرا شده و به متد ()Join می رسد، همانطور که در قبل گفته شد این متد موظف است کار متد اول را به صورت مرتب به پایان رساند و پس از آن نخ های بعدی را به صورت ترکیبی اجرا کند.(با توجه به اینکه CPU وقت هر نخ را مشخص می کند می تواند ترتیب هر یک از نخ های شماره 2و3 در خروجی متفاوت از یکدیگر باشند.)

T1: 0 T1: 1 T1: 2 T1: 3 T1: 4 T1: 5 T1: 6 T1: 7 T1: 8 T1: 9 T1: 10 T3: 21 T3: 22 T3: 23 T3: 24 T3: 25 T3: 26 T3: 27 T3: 28 T3: 29 T2: 11 T2: 12 T2: 13 T2: 14 T2: 15 T2: 16 T2: 17 T2: 18 T2: 19 T2: 20 T3: 30

منابع

1-  سید حسین موسوی، مرجع کامل زبان برنامه نویسیC#.NET

2-  http:// codeproject.com