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

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

Thread (چند نخی)  چیست ؟

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

Multi-threading :
 
Multi-threading برای انجام چندین کار موازی مورد استفاده قرار میگیرد . هر کار میتواند دارای چندین thread باشد . در یک نرم افزار multi-theareded  ،کاربر توانایی بیشتری در انجام نسبت به یک نرم افزار singel-threaded دارد . امروزه ، اکثره نرم افزار ها از multi-threading استفاده میکنند . یک نرم افزار یا یک فرآیند میتواند یک thread رابط کاربری داشته باشد که فعل و انفعالات رخ داده توسط کاربر و thread های فرآیندهایی که در پس زمینه در حال اجرا هستند را مدیریت میکند .
 
برای استفاده از Thread در سی شارپ ابتدا باید از کتاب خانه آن کلاس  زیر را فراخوانی کنیم البته می توانیم بدون این کار هم از کتاب خانه های آن استفاده کنیم البته کار سخت تر می شود.

اگر این کار را انجام ندهیم نیاز است در هنگام استفاده از کلاس Thread سی شارپ کلمه زیر را به آن اضافه کنیم.
پس به این شکل می شود:
 
System.Threading.Thread
 
شکل های مختلفی برای اجرا یک ترد دیگر وجود دارد که در زیر به آن می پردازیم.
 

ساده ترین شکل ایجاد یک ترد در سی شارپ

 

در بالا ما ابتدا void ایجاد شده را در ترد اصلی یا Main Thread اجرا کردیم سپس یک ترد جداگانه ایجاد کرده و در ترد جدید همان void را اجرا کردیم.(به مثال Thread_Example_7 مراجعه شود)
 
در بالا ما تردی را اجرا کردیم ولی ممکن است ما داری چندین ترد در برنامه خود باشیم و نیاز باشد به طور مثال ابتدا ترد اول اجرا شود و به اتمام برسد و سپس ترد دوم اجرا شود خب برای این که ما ترد های خود را با هم هماهنگ کنیم نیاز داریم تا از متد join در ترد استفاده کنیم در زیر یک مثال ساده از گفته های بالا را به کار می بریم.

در مثال بالا اگر ما از متد join استفاده نکنیم ممکن است دستور ها همزمان اجرا شوند و در این صورت مقادیری مانند 0 , 1 , 11 به کاربر نمایش داده می شود . به زبان ساده در هر بار مقادیر مختلف نام برده شده نمایش داده می شود . اما اگر از متد join استفاده شود فقط عدد 11 به کاربر نمایش داده می شود و به این معنی که ابتدا ترد اول اجرا شده و ترد دوم منتطر می ماند تا ترد اول کارش به اتمام رسیده سپس ترد دوم شروع به انجام کار می کند به همین علت فقط عدد 11 به کاربر نمایش داده می شود به این کار  اصطلاحاً هماهنگ سازی یا Synchronization گویند.(به مثال Thread_Example_1 مراجعه شود)
 
برای نگه داشتن یا اصطلاحاً خواباندن ترد می توانیم از متد Sleep استفاده کنیم در زیر درباره ی متد sleep بحث می کنیم.

در بالا ما ابتدا یک پیام برای کاربر نمایش داده سپس از متد sleep از کتاب خانه Thread استفاده کردیم و نکته حائز اهمیت مقدار ورودی متد sleep به Millisceonds یا میلی ثانیه می باشد البته می توانیم از متد Timespan استفاده کنیم تا مقادیر بزرگی را مانند ساعت را به راحتی وارد برنامه کنیم.(به مثال Thread_Example_8 مراجعه شود.)
 

پارامتری کردن یک ترد

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

در بالا ما دو نوع ترد تعریف کردیم اولی یک ترد ساده است که void ما را اجرا می کند . حالا به سراغ ترد دوم می رویم که
یک تغییر کوچک درش می بینید در مثال قبل از ThreadStart برای اجرای void ی که ساخته شده بود استفاده شد اما برای این که یک مقدار ورودی از کاربرگرفته شود و در ترد قرار بگیرد نیاز است که به شکل ParameterizedThreadStart شروع شود در این مثال ما یک مقدار ورودی برای اجرا ترد تعریف کردیم که My name است بعد از اجرای این ترد مقدار My name را  به void دوم ارسال می کند و در void دوم ما یک متغییر از نوع object داریم تا مقدار ورودی را دریافت و از آن مقداربرای چاپ استفاده شده است.(به مثال Thread_Example_3 مراجعه شود.)
 

تفاوت دو متد SpinWait  و Sleep چیست ؟

 
SpinWait یا متوقف کردن چرخش (ایجاد فاصله انجام  زمانی در ترد) یک فاصله زمانی بسیار کوتاه است البته این در پردازنده های Corei7 با 4 هسته واقعی (8 ترد) اگر ما بیشترین مقدار Spinwait را برابر با 1000000000 قراردهیم می بینم که در کمتر از 2 ثانیه این کار انجام می شود. به این معنی که spinwait به شکل یک زمان به کار نمی آید بلکه مقدار کار انجام cpu با x ترد را بررسی می کند یعنی هر چه تعداد این تردها بیشتر باشد spinwait زودتر به پایان می رسد.
 
در مقابل SpinWait متد Sleep قرار دارد وبرعکس SpinWait عمل می کند پس نتیجه میگیرم Sleep تمامی ترد را اصطلاحاً نگه (Suspned می کند) می دارد و پس از یک فاصله زمانی به ادامه کار خود می پردازد.  
(به Thread_Example_4 مراجعه شود)
 

ساخت ترد و استفاده از آن بدون استفاده از void

 
شکل دستوری ساخت یک ترد را دربالاتردیدم هم اکنون نحوه استفاده از آن را بدون استفاده از void بررسی می کنیم.

در بالا ساده ترین شکل ایجاد یک ترد رو بدون استفاده از void بیان شد و در خروجی به کاربر یک پیام نشان داده خواهد شد.
 
(به Thread_Example_10 مراجعه شود)

Thread Priority یا اولویت ترد

 
5 اولویت در ترد وجود دارد که به شکل زیر است
 

  • Highest
  • AboveNormal
  • Normal
  • BelowNormal
  • Lowest


 
در شکل بالا ترد ها رو اولویت بندی کرده  و براساس همان اولویت برای کاربر نشان داده خواهد شد.اولویت بندی برای زمانی نیاز می شود که شما بخواهید چند ترد داشته باشید و تمامی آنها را با هم هماهنگ کنید و بر این اساس شما اولویت های خاصی را برای هر کدام از تردها قرار دهید.(به Thread_Example_11 مراجعه کنید)
 

متدهای کاربری Thread

 

برای ایجاد یک خطا در ترد (استفاده از این متد توضیه نمی شود)Abort
پس از پایان ترد اول در ترد بعدی که اجرا می شود یک delay یا فاصله زمانی ایجاد می شودJoin(TimeSpan)
این متد کاری انجام نمی دهد ! اثری در کارکرد برنامه نمی گذارد – (منسوخ شده)Suspend – (deprecated)
پس از پایان ترد اول در ترد بعدی که اجرا می شود یک delay یا فاصله زمانی ایجاد می شودJoin(MilliSeconds)

 

چهار مورد از سیکل های زندگی (Life Cycles) در ترد :

 

    • حالت اجرا نشده (The Unstarted State)

 

  • حالت آماده اجرا (The Ready State)
  • حالت اجرا نشده (The Not Runnable State)
  • حالت مرده (The Dead State)

 
 

حالت اجرا نشده

 
زمانی است که ترد بعد از تعریف شدن اجرا نشده باشد
برای مثال
Thread td = new Thread(() =>
           {
               Console.WriteLine(“Thread Not Started “);
           });
 
برای اینکه یک ترد اجرا شود نیاز است بعد از تعریف آن تر از متد Start استفاده شود.
 
td.Start();

حالت آماده اجرا

زمانی است که ترد آماده اجرا است و منتظر انجام آن توسط cpu می ماند.
 

حالت اجرا نشده

زمانی است که ترد اجرا نشده است اما تفاوتی که با The Unstarted State دارد در این است که سه حالت سبب اجرا نشدن آن ترد شده است .
 
دلایل عبارتند از :
 

  • Sleep method has been called (زمانی است که از متد sleep استفاده شده است.)
  • Wait method has been called (زمانی است که از متد wait استفاده شده است)
  • Blocked by I/O operations (توسط عمیلیات ورودی خروجی ها بلاک شده باشد.)

 
 

حالت مرده

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

Thread Pool

 
Thread Pool چیست ؟ و چه جایی کاربرد دارد ؟
 
به زبان ساده، thread pool جایی است که تعداد مشخصی thread قرار گرفته‌اند تا تعدادی وظیفه (task) را که غالباً در یک صف قرار دارند، انجام دهند.
 
برای استفاده بهینه از منابع سیستمی بهتر از Thread Pool استفاده می شود اما مزایایی  دارد که در ادامه به آنها می پردازیم.
 
مزایای استفاده از Thread Pool نسبت به اینکه به صورت دستی عملیات ایجاد و فراخوانی thread ها را انجام دهیم در چیست ؟
 
مزایا :
 

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

مزایای ایجاد Thread به صورت دستی
 

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

 
CLR چیست ؟
CLR که مخفف Common Language Runtime میباشد یک زبان میانی است بین برنامه نویسان و سیستم عامل که محیطی یک دست را برای ساخت همه جور برنامه با دات نت ایجاد میکنه. برنامه نویسان زبان VB دیگر محدود به محیط سیستمی ویندوز نیستند و میتوانند مانند برنامه سازان زبان C/C++ برنامه خود را روی سیستم های دیگری که فریم ورک دارند نیز ببینند . و همچنین بخاطر قابلیت های چند زبانه بودن محیط دات نت برنامه نویسان C و VB براحتی کد های نوشته شده توسط دیگری را میتوانند استفاده کنند و میشه گفت در محیط دات نت براحتی میشه یک پروژه را با چند زبان نوشت و در نهایت با هم ترکیب کرد.
 
نحوه به دست آوردن تعداد ترد در کامپیوتر
 
بعضی ها می گویند تعداد هسته برابر است با تعداد Thread اما این اصلا منطقی نیست .
بلکه به سه چیز بستگی دارد : 1- به تعدا هسته واقعی و مجازی 2- به NetFramework که برنامه نویسی با آن برنامه نویسی کرده است.3- به ساختار(معماری) cpu بستگی دارد. 4- به مقدار حافظه Stack  که در برنامه نویسی های low level امکان تعریف مقدار جدید در آن وجود دارد.
 

  • 1023 per core in Framework 4.0 (32-bit environment)
  • 32768 per core in Framework 4.0 (64-bit environment)
  • 250 per core in Framework 3.5
  • 25 per core in Framework 2.0

استفاده از Thread Pooling در سی شارپ

 
ساده ترین شکل استفاده از یک Thread Pool
 

در بالا از متد ThreadPool ویژگی QueueUserWorkItem و مقدار ثابت  state برای تعریف ترد استفاده شده است و همان طور که می بینید در آکولاد تسک هایی که می خواهیم انجام شود را قرار می دهیم.

در بالا یک void به نام Dosomething داریم که یک مقدار ورودی از نوع  object دارد و در Main اصلی یک حلقه داریم و در داخل حلقه  ما از Thread Pool استفاده کردیم همانطور که می بینید بعد  از متد Thread Pool یک ویژگی به نام QueueUserWorkItem داریم که ورودی آن یک WaitCallback است این متد همان طور که از نامش معلوم است منتظر یک مقدار برگشتی می ماند البته باید به این نکته توجه کرد که این متد return ندارد یعنی نمی توانیم چیزی ازش برگردانیم و یکی از معایب Thread Pool محسوب می شود. در صورتی که برای QueueUserWorkItem یک argument  در نظر نگیریم مقدار آن به صورت پیش فرض Null خواهد بود.(در اینجا x یک argument برای QueueUserWorkItem  می باشد.). ( به Thread_Example_12 مراجعه شود.)
 

استفاده از Task در سی شارپ

 

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

 
تفاوت بین Task و Thread در چیست ؟
 

  • Task ها چیزهایی هستند که شما می خواهید انجام دهید.
  • Task ها زمانبندی دقیق تری نسبت به Thread ها دارند.
  • Task ها می توانند Return انجام دهند در صورتی که Thread ها به طور مستقیم این قابلیت را ندارند.
  • کاربردی تر در GUI نسبت به Thread.

 
یک مثال ساده از Task

در بالا از کلاس تسک ویژگی Factory و متد StartTask یک Task جدید ایجاد شده این ساده ارین شکل ایجاد یک Task است و در ادامه یک پیغام چاپ شده است.(به Thread_Example_14 مراجعه کنید. )
 
در ادامه به ایجاد یک شی از کلاس Task می پردازیم

در بالا همانند قبل برای ایجاد Task اقدام شده است فقط تفاوت آن در ایجاد یک کلاس جدید از Task و اجرای آن بوده است.در قبل ما مستقیم Task را اجرا می کردیم اما در انجا بعد از تعریف کلاس در خط آخر Task را اجرا کرده ایم وهمان طور که می بینید دیگر از ویگژگی Factory استفاده نشده است.ما دربالا شماره تردی که برنامه در حال اجرا بروی اون هست رو نمایش داده خواهد شد.
پس برای به دست آوردن شماره ترد می توانیم از Thread.CurrentThread.ManagedThreadId استفاده کنیم.

شش روش برای ایجاد یک Task وجود دارد که عبارتند از :

 

    • روش مستقیم
    • با استفاده از Action
    • با استفاده از delegate
    • با استفاده از Method
    • با استفاده از Task.Run

 

  • با استفاده از Task.FromResult

 
 
روش مستقیم  را  در ابتدای کار با آن آشنا شدیم.
 
روش Delegate یا محول کردن یا زمان بندی شده
 

در این روش در تعریف Task از یکی از ویژگی ها کلاس Task به نام  Delegate استفاده کردیم این باعث می شود زمان بندی میان Task های مختلف هماهنگ باشد. یعنی به طور مثال اگر لازم است ابتدا Task اول شروع شود به Task دوم اجازه نمی دهد تا اجرا شود و Task دوم صبر می کند تا Task اول به طور کامل به پایان برسد سپس Task دوم اجرا می شود فقط باید دقت شود در اینجا ما Task اول را از ویژگی Wait آن استفاده کردیم این سبب هماهنگی بیشتر میان Task های مختلف می شود و باید همیشه یک بارکمتر از کل Task های ما باشد.در اینجا مقداری درون ویژگی Wait قرار داده نشده است ولی می توان به آن به صورت دستی نیز عدد داد.(به Thread_Example_16 مراجعه شود.)

 
ویژگی Wait چهار ورودی را می تواند دریافت کند که به شکل زیر است.
 

به شکل ساده بدون ورودیWait()
منتظر token از یک task برای لغو کردن Wait(CancellationToken)
مقدار عددی (int32)Wait(Int32)
گرفتن مقدار از نوع TimeSpanWait(TimeSpan)

 

روش Action

 

 
در این روش که همانند قبل است و فقط به جای Delegate از Action استفاده شده است و سپس یک void را اجرا کرده است.(به Thread_Example_17 مراجعه شود.)
 
 

مطالعه بیشتر