هماهنگی بین نخ ها :
اما استفاده از نخ ها ، یا در واقع استفاده از چند هسته بصورت همزمان ، کار ساده ای نیست . یا در واقع میشه گفت جزء پیچیده ترین کارهاست .
این پیچیدگی بخاطر اینه که بخشی از کدی که نوشتیم و در اون بخش از کد، اعضایی را مشترکا در چند نخ بکار بردیم (مثل متغییرهای سراسری یا گاها متغییرهایی که به نخ ها pass داده میشه یا متدهایی که در چند نخ فراخونی میشن)، در بسیاری از موارد لازم میشه که این اعضا با هم هماهنگ یا همگام یا synchronize بشن .
چون اون عضو (عضوِ مشترک ای که در چند نخ بکار برده شد) ، به احتمال زیاد ممکنه بصورت همزمان همه ی اون نخ ها بهش دسترسی داشته باشن ، بنابراین مشکلات زیاد و متفاوتی ممکنه رخ بده (مثلا ممکنه نخِ 1 ، مقدار یک داده و متغییر مشترک را بنویسه و نخ 2 ، مقدار اون متغییر را در همون زمان بخونه در صورتی که نخ 2 ، انتظار داشت مقدار قبلی را بخونه) .
واسه ی همین ، برای اون بخش از کدی که نوشتیم و در اون بخش از کد، اعضایی را مشترکا در چند نخ بکار بردیم ، باید جوری برای اون نخ ها محدودیت بذاریم که نخ ها ، با ترتیب خاصی اجرا بشن .
مثلا وقتی به اون بخش از کد رسید ، فقط یک نخ اون بخش از کد را اجرا کنه و وقتی اون نخ ، اون بخش از کد را اجرا کرد ، نخ دیگه ای که توی صف هست ، اون بخش از کد را اجرا کنه . بنابراین چون در یک نخ اجرا میشه ، اون بخش از کد ، فقط توسط یک هسته ی منطقی اجرا میشه . بنابراین اون بخش از کد بصورت همزمان اجرا نمیشه . البته محدودیت هایی که میشه اعمال کرد ، بسیار زیاده . این مثال ، مثال کلی بود .
یا مثلا اینکه 2 نخ ، بصورت همزمان اجرا بشن و نخ بعدی ، بعد از اتمام اون بخش از کد ، اجرا بشه . یا مثلا اینکه بجای اینکه محدودیت رو روی بخش خاصی از کد بذاریم ، روی کل نخ میتونیم بذاریم . مثلا تا کل نخ 1 تموم نشد ، نخ های بعدی ، اجرا نشن .
به این عملیات ، یعنی عملیاتی که روی کدها (حالا بخشی یا کلش و ...) محدودیت اعمال کنه تا اعضای مشترکی که در چند نخ استفاده شد ، به اون منوالی و روشی پیش برن که برنامه نویس به خواسته اش برسه و (این اعضا) با هم هماهنگ عمل کنن را هماهنگ سازی یا همگام سازی یا synchronize میگن .
در همگام سازی ، معمولا (نه همیشه) بقیه ی نخ ها را (در بخشی از کد) متوقف میکنن و نخ جاری ای که در حال اجرای اون کد که هست و کارش تموم شد ، بعد دونه دونه ی نخ هایی که در صف هستند ، اون هم یکی یکی ، کارشون را انجام بدن ولی باز هم روش ها متفاوت هه .
هر چی الگوریتم (استفاده شده در نخ ها) هم پیچیده تر باشه ، کار هماهنگ سازی ، دشوارتر میشه .
کد زیر را نگاه کن (کنترلی بنام listBox2 لازم هست) :
کد:private long globalVar_20; private void ThreadSync2_Click(object sender, EventArgs e) { this.listBox2.Items.Clear(); Thread thread_2 = new Thread(this.ThreadMethod_20); thread_2.Name = "Thread 2"; thread_2.Start(thread_2.Name); Thread thread_3 = new Thread(this.ThreadMethod_20); thread_3.Name = "Thread 3"; thread_3.Start(thread_3.Name); } private void ThreadMethod_20(object param) { string threadName = (string)param; for (long i = 0; i < 10; i++) { this.globalVar_20++; this.Invoke(new MyDelegate(this.AddToListBoxInMainThread), this.globalVar_20.ToString() + " " + threadName); } } private delegate void MyDelegate(string str); private void AddToListBoxInMainThread(string str) { this.listBox2.Items.Add(str); }
دو نخ (متغییرهای thread_2 و thread_3) ، که این دو نخ ، هر کدوم ، همزمان ، متد ThreadMethod_20 را اجرا میکنن (یعنی متد ThreadMethod_20 ، همزمان 2 بار اجرا میشه . یه بار در نخ thread_2 و یه بار هم در نخ thread_3).
همونطور که میبینی و میدونی ، چند بار نخ thread_2 ، و چند بار نخ thread_3 اجرا میشه . ضمنا ، هیچ تضمینی هم نیست که مقدارشون به ترتیب اضافه بشه . (البته الگوریتم این ، زیاد جالب نیست و حالا مثال ساده هست دیگه . هر چند ، این مثال ، گویای کاملِ این مشکل نمیتونه باشه) .
بنابراین لازم داریم که این بخش از کد که متغییر سراسریِ globalVar_20 را ازش استفاده میکنیم ، فقط یک نخ اون بخش را اجرا کنه و بعد از اینکه اون بخش از کد تمام شد ، نخ دیگه کارش را انجام بده . در واقع میخوایم برای این بخش از کد ، همگام سازی یا synchronize انجام بدیم .
برای همگام سازی ، از کلاس ها و استراکچرهای زیادی در دات نت میشه استفاده کرد مثل کلاس Monitor و Mutex و WaitHandle (و فرزندان کلاس WaitHandle مثل EventWaitHandle, AutoResetEvent, ManualResetEvent) و Semaphore و SemaphoreSlim و ... .
خود کلاس Thread هم متدی بنام Join داره که نوع دیگه ای از همگام سازی را انجام میده .
اگه اشتباه نکنم ، کلاس ها و استراکچرهای SpinLock و ReaderWriterLockSlim و CountdownEvent و Barrier و Interlocked و SpinWait هم هستن .
هر کدوم از این کلاس ها (و اعضا) ، نوع خاصی از همگام سازی با شرایط خاصی را پیاده سازی میکنن .
همچنین کلمه ی کلیدی کاربردی ای بنام lock در سی شارپ وجود داره که از کلاس Monitor (متد Monitor.Enter و متد Monitor.Exit) استفاده میکنه . یعنی اگه بصورت حرفه ای نخوایم از کلاس Monitor استفاده کنیم ، میتونیم از کلمه ی کلیدی lock برای همگام سازی استفاده کنیم . در این آموزش ، ما از این کلمه ی کلیدی برای همگام سازی استفاده میکنیم .
ساختار کلمه ی کلیدی lock ، بصورت زیر هست :
که در قسمت instance ، شی ای از نوع reference type (مثل شیِ کلاس ها) بهش میدیم . البته این شی ، نباید شی this و همچنین شی ای از string ها باشه (یه نکته ی دیگه این شی داشت که فراموش کردم) . البته من متوجه نشدم این شی برای چی هست .کد:lock (instance) { // code block }
همونطور که میبینی ، lock ، یه بلاک داره که کدهای مورد نظر (هر کدی) را توش مینویسیم . هر نخی وارد این بلاک از کد بشه ، نخ های دیگه ، متوقف میشن و نمیتونن وارد این بلاک بشن تا کدهاش را اجرا کنن . به عبارتی دیگه ، داخل بلاک lock ، همزمان ، فقط یک نخ اون کد را اجرا میکنن و از این طریق مطمئن میشیم که اون عضوِ مشترک مون ، فقط توسط یک نخ در همون زمان در دسترس هست .
کد بالا را در بلاک lock اجرا میکنیم :
کد:private delegate void MyDelegate(string str); private void AddToListBoxInMainThread(string str) { this.listBox2.Items.Add(str); } private long globalVar_20; private void ThreadSync2_Click(object sender, EventArgs e) { this.listBox2.Items.Clear(); Thread thread_2 = new Thread(this.ThreadMethod_20); thread_2.Name = "Thread 2"; thread_2.Start(thread_2.Name); Thread thread_3 = new Thread(this.ThreadMethod_20); thread_3.Name = "Thread 3"; thread_3.Start(thread_3.Name); } private object myLock_1 = new object(); private void ThreadMethod_20(object param) { string threadName = (string)param; lock (myLock_1) { for (long i = 0; i < 10; i++) { this.globalVar_20++; this.Invoke(new MyDelegate(this.AddToListBoxInMainThread), this.globalVar_20.ToString() + " " + threadName); } } }
همونطور که میبینی در این کد ، به ترتیب ، اول یک نخ بصورت کامل اجرا میشه و بعد از خروجِ یک نخ از بلاک lock ، نخ بعدی شروع به اجرا میکنه .
دقت کن که هر کدی که قبل و بعد از بلاک lock (درون همون متد) نوشته بشه ، بصورت همزمان توسط نخ ها اجرا میشن و فقط کد داخل lock هست که در همون لحظه ، فقط توسط یک نخ اجرا میشه ها .
و چون lock باعث توقف (موقتی) بقیه ی نخ ها میشه و پس علاوه بر اجرا درون یک هسته ، باعث میشه (اگه بقیه ی نخ ها روی چند هسته ی منطقی اجرا شده بودن ، کدشون را موقتا توقف بدن) اون هسته های منطقی ، به نخ های دیگه از یه پروسه (های) دیگه (در صورت وجود) ، سوئیچ کنن و دوباره بعد از اتمام بلاک lock ، به نخ ما سوئیچ کنن ، پس در ازای هر بار استفاده از lock ، علاوه بر اینکه اون بخش از کد lock ، فقط در یک هسته ی منطقی اجرا میشه (که خود همین ، باعث کاهش کارایی میشه) ، بعد از اتمام بلاک lock توسط یک نخ ، هسته ی منطقی ای باعث بشه سوئیچ روی نخ ما انجام بده و چون هر سوئیچی سربار داره ، این ، دومین مشکل کاهش کارایی lock ها خواهد بود .
در این کد ، همونطور که میدونی ، اگه lock را فقط در قسمتِ کدِ globalVar میذاشتیم ، یعنی این جوری مینوشتیم :
کد:for (long i = 0; i < 10; i++) { lock (myLock_1) { this.globalVar_20++; } this.Invoke(new MyDelegate(this.AddToListBoxInMainThread), this.globalVar_20.ToString() + " " + threadName); }
همونطو که گفتم ، هم اون حلقه چون بیرون از بلاک lock هست (بجز قسمتِ بلاک lock) ، بصورت همزمان توسط نخ ها اجرا میشد و هم بخاطر اینکه به تعداد حلقه ، lock هم استفاده میشه ، (پس به همین تعداد ضربدر تعداد نخ های اجرا کننده ی اون متد در اون لحظه) ، هسته های منطقی باید سوئیچ انجام بدن .
بنابراین این کد را اگه امتحان کنی ، میبینی خیلی خیلی کندتر از کد قبلی (که سراسر حلقه ی for را lock کرده بودیم) اجرا میشه . این کندی زمانی بیشتر مشخص میشه که تعداد حلقه ها بالاتر بره . مثلا تعداد حلقه ی for بیشتر از 5000 تا بشه .
نکته ی بعدی اینکه دسترسی به اعضای سراسری مثل فیلدها و مخصوصا اعضای static در نخ ها ، خیلی کندتر از اعضای محلی هست (شاید ده برابر کندتر) .
کدی که فقط اعضای متغییر محلی استفاده میشه :
کد:private void ThreadSync2_Click(object sender, EventArgs e) { this.listBox2.Items.Clear(); Thread thread_2 = new Thread(this.ThreadMethod_20); thread_2.Name = "Thread 2"; thread_2.Start(thread_2.Name); Thread thread_3 = new Thread(this.ThreadMethod_20); thread_3.Name = "Thread 3"; thread_3.Start(thread_3.Name); } private object myLock_1 = new object(); private void ThreadMethod_20(object param) { for (long i = 0; i < 1000000000; i++) { } }
کدی که عضو متغییر سراسری هم استفاده میشه :
کد:private long globalVar_20; private void ThreadSync2_Click(object sender, EventArgs e) { this.listBox2.Items.Clear(); Thread thread_2 = new Thread(this.ThreadMethod_20); thread_2.Name = "Thread 2"; thread_2.Start(thread_2.Name); Thread thread_3 = new Thread(this.ThreadMethod_20); thread_3.Name = "Thread 3"; thread_3.Start(thread_3.Name); } private object myLock_1 = new object(); private void ThreadMethod_20(object param) { for (long i = 0; i < 1000000000; i++) { this.globalVar_20++; } }
علتش را دقیقا نمیدونم اما اگه اشتباه نکنم بخاطر این باشه که متغییر سراسری چون باید همیشه در دسترس باشه ، پس باید توی حافظه ی رم ذخیره بشه اما متغییرهای محلی چون بصورت موقتی ایجاد میشن و بعد از اون بلاک از دست میرن ، شاید بتونن کلا توی حافظه ی کش باشن . دقیق نمیدونم .
چون break point هم توی دو نخ بصورت همزمان کار نمیکنه (تا جایی که میدونم) و حداقل اینکه trace کردن چند نخ ، خیلی سخت تر از تک نخ هست ، بر پیچیدگی های دیباگینگ برنامه های چند نخی ، اضافه میکنه .
دقت کن که lock زمانی کاربرد داره که یک متد واحد ، در چند نخ متفاوت اجرا بشه (نه اینکه مثلا دو متد متفاوت درون دو نخ متفاوت اجرا بشه) . مثل کد بالا .
اگه چند متد متفاوت درون چند نخ متفاوت اجرا بشن ، در اون صورت از کلاس های دیگه (که چندین تاشون را نام بردم) برای همگام سازی استفاده میکنن . یا از متد Join در کلاس Thread استفاده میکنن .
بنابراین ، چون همگام سازی ، سربار داره و کلا کاهش کارایی داره ، تا جایی که ممکنه ، سعی میشه در چند نخی ها ، از اعضای مشترک استفاده نشه . در وهله ی اول ، فقط از متغییرهای محلی استفاده بشه . اگه قرار به استفاده از متغییرهای سراسری شد ، سعی بشه از متغییرهای جداگانه برای هر نخ استفاده بشه (هر نخ ، متغییرِ جداگانه ی مربوط به خودش را داشته باشه) و در نهایت اگه از اعضای مشترک استفاده شد ، در صورت نیاز ، باید همگام سازی بشه که همگام سازی هم باعث کاهش کارایی میشه اما سعی میشه کمترین کاهش کارایی را داشته باشه (با استفاده از الگوریتم و کدنویسی و کلاس (و روش) مناسب برای همگام سازی) .
مقالات دیگه تمام شد ( البته احتمالا برای مشاهده این لینک/عکس می بایست عضو شوید ! برای عضویت اینجا کلیک کنید ) . حالا بریم سر وقت جواب پست 1 ات برای مشاهده این لینک/عکس می بایست عضو شوید ! برای عضویت اینجا کلیک کنید .
موفق باشی .
Bookmarks