Multithreading با Net.

در این مقاله چگونگی‌ کار Multithreading را شرح میدهیم.شما میاموزید که یک سیستم عامل چگونه اجرای thread‌ها را مدیریت می‌کند و به شما نشان میدهیم که چگونه کلاس‌های thread را در برنامه دست کاری کنید تا بتوانید thread‌های تحت مدیریت خود را ایجاد و آغاز کنید. در این مقاله ایجاد thread ،race condition‌ها ، deadlock‌ها ، monitor ، mutex ، synchronization و semaphore‌ها را بررسی‌ می‌کنیم.

Multithreading با  Net.

در این مقاله توضیح می‌‌دهیم که چگونه با استفاده از فضای نام System.Threading برنامه‌های multithreading را کد بزنیم. Multithreading در برنامه‌ها میتواند باعث به وجود آمدن مباحث Concurrency شود ، مانند Race Condition و Deadlock ها.
و در آخر در این مقاله در مورد تکنیک‌های مختلف Synchronization بحث می‌کنیم ، مانند Lock‌ها ، Mutex و Semaphore‌ها که برای مدیریت مشکلات Concurrency و حمایت از thread‌ها هستند.

 


مرور کلی‌ Multithreading

thread یک جریان مستقل از دستورالعمل‌های یک برنامه است.thread همانند یک برنامه ثانویه است.با این حال ،خود thread یک برنامه نیست،thread نمی تواند روی خودش اجرا شود و به جای آن درون context یک برنامه اجرا میشود.

کاربرد واقعی thread در مورد تنها یک thread پی‌ در پی‌ نیست، بلکه در مورد چندین thread در یک برنامه است.چندین thread به صورت هم زمان اجرا میشوند و کارهای متفاوتی را انجام میدهند (multithreading) . یک thread به عنوان یک  فرایند سبک وزن در نظر گرفته شده است چون درون محتوای یک برنامه اجرا میشود و از resource‌هایی‌ که به آن برنامه تخصیص داده شده است استفاده می‌کند.
Multithreading1.jpg


با استفاده از task manager ، می توانید ستون thread را باز کنید و process‌ها و تعداد هر thread در هر process را ببینید.در این جا میتوانید ببینید که تنها process‌ ای‌ که دارای یک thread است cmd.exe است در حالیکه تمام application‌های دیگر از چندین thread استفاده میکنند.

(اگر ستون thread فعال نیست ،در task manager به tab view بروید ،گزینهٔ select columns را انتخاب کنید ،و در آن جا تیک thread را بزنید)

Multithreading2.jpg

سیستم عامل زمان بندی thread هارا انجام میدهد. هر thread دارای اولویت است و همچنین هر thread دارای stack مربوط به خودش است، اما حافظهٔ کد برنامه و stack در بین همهٔ thread‌های یک برنامه تقسیم میشود.

هر process شامل یک یا چند thread اجرایی است. هر process همیشه شامل حداقل یک thread است که به آن thread اصلی‌ می‌گوییم. (مانند متد Main() در #C) پس process‌ای‌ که Single-thread است فقط شامل یک thread است در حالیکه ، process‌ای‌ که multithread است شامل بیش از یک thread برای اجرا است.

در کامپیوتر،سیستم عامل application هارا load و آغاز می‌کند.هر application یا سرویس به صورت process جداگانه روی ماشین اجرا میشود. عکس زیر نشان میدهد که در واقع تعداد process‌هایی‌ که در حال اجرا هستند از application‌هایی‌ که روی سیستم در حال اجرا هستند بیشتر است. تعداد زیادی از این process‌ها process‌های پشت صحنهٔ سیستم عامل هستند که به صورت اتوماتیک وقتی‌ سیستم عامل load میشود آغاز به کار میکنند.

Multithreading4.jpg


فضای نام System.Threading

همانند ویژگی‌‌های بسیار زیاد دیگری که  در NET. هست ، System.Threading هم یک فضای نام است که type‌های  مختلفی‌ را برای کمک به ما در ساخت application‌های multithread ،  فراهم کرده است.

کلاس System.Threading.Thread

کلاس thread به شما این امکان را میدهد تا thread‌های مدیریت شده توسط خودتان را اجرا و مدیریت کنید.این thread هارا thread‌های مدیریت شده می‌گوییم.


پیاده سازی Multithread

بدست آوردن اطلاعات مربوط به Thread جاری:

برای نمایش استفادهٔ ابتدایی از Thread ، فرض کنید یک console application دارید که در آن ویژگی‌‌های thread جاری وجود دارد و یک شی‌ از جنس thread وجود دارد که thread در حال اجرا را نشان میدهد.

using System;

using System.Threading;

 

namespace threading

{

    class Program

    {

        static void Main(string[] args)

        {

            Console.WriteLine("**********Current Thread Informations***************\n");

            Thread t = Thread.CurrentThread;

            t.Name = "Primary_Thread";

 

            Console.WriteLine("Thread Name: {0}", t.Name);

            Console.WriteLine("Thread Status: {0}", t.IsAlive);

            Console.WriteLine("Priority: {0}", t.Priority);

            Console.WriteLine("Context ID: {0}", Thread.CurrentContext.ContextID);

            Console.WriteLine("Current application domain: {0}",Thread.GetDomain().FriendlyName);

           

            Console.ReadKey(); 

        }

               

    }

}


بعد از compile شدن application ، خروجی به صورت زیر خواهد بود :

Multithreading5.jpg


ایجاد یک Thread ساده


مثال زیر پیاده سازی یک کلاس Thread را شرح میدهد که در آن سازنده‌ی کلاس Thread یک پارامتر را به عنوان نماینده قبول می‌کند.بعد از ایجاد شی‌ از کلاس Thread ،میتوانید thread را با استفاده از متد ()Start همانند زیر شروع کنید:

using System;

using System.Threading;

 

namespace threading

{

    class Program

    {

        static void Main(string[] args)

        {

            Thread t = new Thread(myFun);

            t.Start();

 

            Console.WriteLine("Main thread Running");

            Console.ReadKey(); 

        }

 

        static void myFun()

        {

            Console.WriteLine("Running other Thread"); 

        }

    }

}


بعد از اجرا ، خروجی‌ دو thread همانند زیر خواهد بود :

Multithreading6.jpg

در اینجا نکتهٔ بسیار مهمی که وجود دارد این است که ، تضمینی وجود ندارد که کدام خروجی اول بیاید، به عبارت دیگر مشخص نیست که کدام thread اول شروع شود.thread‌ها به وسیلهٔ سیستم عامل زمانبندی می شوند .بنابر این ،اینکه کدام thread اول بیاید ممکن است در هر زمان متفاوت باشد.


Thread‌های پس زمینه (background Thread)

process‌های application‌ها تاجایی به اجرا ادامه می دهند که حداقل یک thread پیش زمینه در حال اجرا باشد (foreground thread) .اگر بیشتر از یک foreground thread در حال اجرا باشد و متد () Mainتمام شود process مربوط به application تا زمانی‌ که تمام foreground thread‌ها کار خود را به پایان برسانند ،به فعالیت خود ادامه میدهد و با اتمام کار foreground thread ، تمام foreground thread‌ها بلافاصله خاتمه پیدا میکنند.

هنگامی که بوسیله کلاس Thread یک thread ایجاد می‌کنید،میتوانید با استفاده از قرار دادن تنظیمات روی IsBackground برای آن تعریف کنید که thread‌یی‌ که ایجاد می‌کنید foreground باشد یا background. متد اصلیMain ، مproperty مربوط به thread t را false می‌کند.بعد از set کردن thread جدید ، thread اصلی‌ برای console یک پیغام خاتمه می‌نویسد.thread جدید یک پیغام شروع و خاتمه می‌نویسد و در این بین ۲ ثانیه sleep می‌کند.
 

using System;

using System.Threading;

 

namespace threading

{

    class Program

    {

        static void Main(string[] args)

        {

            Thread t = new Thread(myFun);

            t.Name = "Thread1";

            t.IsBackground = false; 

            t.Start();

            Console.WriteLine("Main thread Running");

            Console.ReadKey(); 

        }

 

        static void myFun()

        {

            Console.WriteLine("Thread {0} started", Thread.CurrentThread.Name);

            Thread.Sleep(2000); 

            Console.WriteLine("Thread {0} completed", Thread.CurrentThread.Name); 

        }

    }

}

 

هنگامی که این application را کامپایل کنید ،پیغام اتمام را که در console نوشته شده است می‌بینید،چون thread جدید، foreground بود.

در زیر خروجی را می‌بینید:

Multithreading7.jpg

اگر property مربوط به thread یعنی‌ IsBackground را true کنیم خروجی به صورت زیر خواهد بود:

Multithreading8.jpg

 


بحث Concurrency  (همزمانی)

هنگامی که multithread‌یی‌ را آغاز می‌کنیم که به داده‌های یکسان دسترسی‌ دارند،برنامهٔ شما نیاز دارد اطمینان حاصل کند که هر قسمت از دادهٔ به اشتراک گذاشته شده، در مقابل thread‌های بزرگی‌ که می‌‌خواهند value مربوط به thread را عوض کنند محافظت شود.

Race Condition

اگر ۲ یا بیشتر thread ،به یک شی‌ یکسان دسترسی‌ داشته باشند ، یا به یک state به اشتراک گذاشته شده که هنوز synchronize نشده باشد دسترسی‌ داشته باشند ، در این صورت race condition اتفاق می‌‌افتد.برای نشان دادن مشکل Race Condition ، یک console application ایجاد کنید.این application از یک کلاس به نام Test استفاده می‌کند تا ۱۰ عدد را چپ کند.نحوه یه عمل به وسیلهٔ توقف thread جاری برای چندین بار است، که این چندین بار به صورت رندوم انتخاب می‌‌شود.

 

Using System;

using System.Threading;

 

namespace threading

{

    public class Test

    {

        public void Calculate()

        {  

            for (int i = 0; i < 10; i++)

            {

                Thread.Sleep(new Random().Next(5));

                Console.Write(" {0},", i);

            }

            Console.WriteLine();

        }

    }

    class Program

    {

        static void Main(string[] args)

        {

            Test t = new Test();

            Thread[] tr = new Thread[5];

 

            for (int i = 0; i < 5; i++)

            {

                tr[i] = new Thread(new ThreadStart(t.Calculate));

                tr[i].Name = String.Format("Working Thread: {0}", i);

            }

 

            //Start each thread

            foreach (Thread x in tr)

            {

                x.Start();

            }

            Console.ReadKey();

        }       

    }

} 


بعد از کامپایل برنامه، thread اصلی‌ درون محدودهٔ application ، با تولید ۵ thread ثانویه شروع به کار می‌کند .به هر کدام از thread‌های در حال کار گفته می‌‌شود که متد Calculate() را در همان کلاس Test فراخوانی کنند.از این جهت ، تمام ۵ thread  شروع میکنند به دستیابی به متد Calculate() به طور همزمان، و تا زمانی‌ که کاری در رابطه با پیش بینی‌ و پیشگیری در مورد قفل کردن  resource‌هایی‌ که برای شی‌ به اشتراک گذاشته شده نکرده ایم؛این کار منجر به Race Condition میشود و application خروجی غیر قابل پیش بینی‌‌یی‌ را همانطور که می‌بینید تولید می‌کند:


Multithreading9.jpg

 

Deadlock


وجود lock کردن بسیار زیاد در application  میتواند application شمارا به مشکل بیندازد.در Deadlock ،حداقل ۲ thread برای یکدیگر صبر میکنند تا lock‌ی‌ را باز کنند.در این هنگام Deadlock اتفاق می‌افتاد و thread‌ها بدون وقفه صبر میکنند و برنامه ،پاسخ را متوقف می‌کند.

در اینجا هر دو متد state شی‌ obj۱ و obj۲ را به وسیلهٔ lock کردن آنها عوض می‌کند.متد ()Deadlock۱ ابتدا obj۱ و سپس obj۲ را lock می‌کند و بعد متد مشابه ()Deadlock۲ ابتدا obj۲ و سپس obj۱ را lock می‌کند.بنابر این lock برای obj۱ به پایان می رسد و obj۱ آزاد میشود ،سپس تعویض thread اتفاق می‌افتاد و متد دوم آغاز به کار می‌کند و به lock برای obj۲ میرسد.در این هنگام thread دوم صبر می‌کند تا  lock ،obj۱شود.حالا هردو thread صبر میکنند و یکدیگر را آزاد نمی کنند.این یک وضیعیت Deadlock است.
 

using System;

using System.Threading;

 

namespace threading

{   

    class Program

    {

        static object obj1 = new object();

        static object obj2 = new object();

 

        public static void DeadLock1()

        {

            lock (obj1)

            {

                Console.WriteLine("Thread 1 got locked");

                Thread.Sleep(500);

               
                lock (obj2)

                {

                    Console.WriteLine("Thread 2 got locked");

                }

            }

        }

 

        public static void DeadLock2()

        {

            lock (obj2)

            {

                Console.WriteLine("Thread 2 got locked");

                Thread.Sleep(500);

               
                lock (obj1)

                {

                    Console.WriteLine("Thread 1 got locked");

                }

            }

        }

 

        static void Main(string[] args)

        {

            Thread t1 = new Thread(new ThreadStart(DeadLock1));

            Thread t2 = new Thread(new ThreadStart(DeadLock2));

 

            t1.Start();

            t2.Start();

 

           Console.ReadKey();

        }

 

       

    }

}

 

Synchronization (هماهنگ سازی )


به وسیله Synchronization می‌توان از مشکلاتی‌ که با استفاده از Multithread‌ها به وجود می آمد ،مانند Race condition و deadlock ، دوری کرد.
همیشه پیشنهاد می شود که با اشتراک نگذاشتن داده بین  thread‌ها از به وجود آمدن Concurrency اجتناب کنید.البته این کار همیشه امکان پذبر نیست .
اگر اشتراک داده غیر قابل اجتناب باشد شما مجبورید که از Synchronization استفاده کنید ،بنابر این فقط یک thread در یک زمان قابل دسترسی‌ است و می تواند state اشتراک گذاری را تغییر بدهد.
در این بخش تکنیک‌های متفاوتی از Synchronization را تشریح می‌کنیم.

Lock ها

ما می‌‌توانیم دسترسی‌ به منابع به اشتراک گذاشته شده را با استفاده از کلمه synchronize ، lock   کنیم ،با انجام این کار ،thread‌های ورودی نمیتوانند thread جاری را متوقف کنند و جلوی اتمام کارش را بگیرند.کلمه کلیدی lock نیاز به object reference دارد.

با برطرف کردن مشکل Race condition پیشین،می‌ توانیم این برنامه را به وسیلهٔ پیاده‌سازی یک lock روی آن statement تصحیح کنیم تا آن را از race condition محفوظ بداریم:

public class Test

{

    public object tLock = new object();

 

    public void Calculate()

    {

        lock (tLock)

        {

            Console.Write(" {0} is Executing",Thread.CurrentThread.Name);

 

            for (int i = 0; i < 10; i++)

            {

                Thread.Sleep(new Random().Next(5));

                Console.Write(" {0},", i);

            }

            Console.WriteLine();

        }

    }

}


بعد از compile این برنامه ،این بار همانطور که می‌بینید نتیجهٔ دلخواه تولید می‌‌شود.در اینجا ،هر thread از موقعیّت خود استفاده می‌کند تا وظایفش را به پایان برساند.

Multithreading10.jpg


Monitor

lock statement به وسیلهٔ compiler مصمم به استفاده از کلاس Monitor می شود.کلاس Monitor بیشتر شبیه به کلاس lock است ولی‌ مزیت آن نسبت به lock, کنترل بهتر است.شما میتوانید ورود و خروج lock را همانطور که در کد زیر آمده است یاد بگیرید.

object tLock = new object();
 

public void Calculate()

{

    Monitor.Enter(tLock);

    try

    {

      for (int i = 0; i < 10; i++)

      {

        Thread.Sleep(new Random().Next(5));

        Console.Write(" {0},", i);

      }

    }

    catch{}

    finally

    {

      Monitor.Exit(tLock);

    }

    Console.WriteLine();

}




در حقیقت اگر شما کود IL هر application را که از lock استفاده می‌کند ببینید،refrence کلاس Monitor را می‌بینید.همانند زیر:
 

Multithreading11.jpg

استفاده از صفت [Synchronization]

صفت [Synchronization] عضوی از فضای نام System.Runtime.Remoting.Context است.این صفت مربوط به level کلاس‌ها ،به طور موثری تمام instance‌های object را برای ایمنی lock down ، thread ،  می‌کند.
 

 

using System.Threading;

using System.Runtime.Remoting.Contexts; 

 

    [Synchronization]

    public class Test:ContextBoundObject

    {

 

        public void Calculate()

        {

 

            for (int i = 0; i < 10; i++)

            {

                Thread.Sleep(new Random().Next(5));

                Console.Write(" {0},", i);

            }

 

            Console.WriteLine();

        }

    }

 


Mutex

Mutex مخفف Mutual Exclusion (حذف متقابل) است ،که synchronization را در طول اجرای multiple thread عرضه می‌کند. کلاس Mutex از WaitHandle مشتق شده است ،شما می‌‌توانید متد WaitOne() را اجرا کنید تا mutex lock را پیدا کنید و مالک mutex در آن لحظه شوید.mutex به وسیلهٔ فراخوانی متد ReleaseMutex() همانطور که در زیر می‌بینید آزاد میشود:

 

using System;

using System.Threading;

 

namespace threading

{   

    class Program

    {

        private static Mutex mutex = new Mutex();

 

        static void Main(string[] args)

        {

            for (int i = 0; i < 4; i++)

            {

                Thread t = new Thread(new ThreadStart(MutexDemo));

                t.Name = string.Format("Thread {0} :", i+1);

                t.Start();

            }

            Console.ReadKey();

        }

 

        static void MutexDemo()

        {

            try

            {

                mutex.WaitOne();   // Wait until it is safe to enter.

                Console.WriteLine("{0} has entered in the Domain",

                    Thread.CurrentThread.Name);

 

                Thread.Sleep(1000);    // Wait until it is safe to enter.

                Console.WriteLine("{0} is leaving the Domain\r\n",

                    Thread.CurrentThread.Name);

 

            }

            finally

            {

                mutex.ReleaseMutex();

            }

        }

    }

}


زمانیکه با موفقیت برنامه را compile کنید ،نشان میدهد که چه زمانی‌ هر thread جدیدی اولین ورودش را به domain برنامه شروع کرده است.زمانیکه وظایف خود را تمام می‌کند،آزاد میشود و دومین thread شروع به کار می‌کند و الی آخر.
 

Multithreading12.jpg

Semaphore

Semaphore خیلی‌ شبیه به mutex است اما یک semaphore میتواند یکسره با multiple thread‌ها استفاده شود در حالیکه Mutex نمیتواند.با semaphore میتوانید بفهمید که چه تعداد thread اجازه دارند به resource‌هایی‌ که توسط semaphore محافظت شده اند به طور همزمان دسترسی‌ داشته باشند.
در مثال زیر،  ۵thread  و  ۲semaphore ایجاد شده اند.در constructor کلاس semaphore ،میتوانید تعداد lock‌هایی‌ را که با semaphore به وقوع  می‌پیوندند را تعریف کنید.

using System;

using System.Threading;

 

namespace threading

{

   

    class Program

    {

        static Semaphore obj = new Semaphore(2, 4);

 

        static void Main(string[] args)

        {

            for (int i = 1; i <= 5; i++)

            {

                new Thread(SempStart).Start(i);

            }

           

            Console.ReadKey();

        }

 

        static void SempStart(object id)

        {

            Console.WriteLine(id + "-->>Wants to Get Enter");

            try

            {

                obj.WaitOne();

                Console.WriteLine(" Success: " + id + " is in!");                         

 

                Thread.Sleep(2000);             

                Console.WriteLine(id + "<<-- is Evacuating");      

            }

            finally

            {

                obj.Release();

               

            }

        }

    }

}


زمانیکه این application را اجرا می‌کنیم ،۲semaphore فورا ایجاد میشوند و بقیه صبر میکنند چون ما ۵thread ایجاد کرده ایم ،بنابر این ۳تا از آنها  در waiting state میماند.در این حرکت ،هر کدام از thread‌ها که آزاد شوند،بقیه thread‌ها یکی‌ یکی‌ ایجاد می‌‌شوند.

مانند زیر:
 

Multithreading13.jpg