سلامی مجدد .
خیلی ممنون از پست خوب مهندس آرمین . اطلاعات ارزشمندی رو گفتن .
میخواستم پست شماره ی 3 را ویرایش کنم که غیر فعال شد . با این فعالیت در طول این سال ها در انجمن ، خیلی وقته خواستار این هستیم که پست خودمون را بتونیم ویرایش و حذف کنیم ولی این مجوز را نداریم (بعضی انجمن ها ، ویرایش پست کاربر ، از همون اول براش مهیاست) . بنابراین ، نکته ی تکمیلی و ویرایشی را در اینجا میگم . در قسمت اول در پست شماره ی 3 که گفته بودم :
"البته ، این ، به این معنی نیست که اگه یه پروسه ای ، بصورت پیش فرض ، ۴ تا نخ را همزمان اجرا کرده باشه ، این نخ ها در ۴ هسته ی فیزیکی اجرا میشن پس بنابراین پردارشِ این ۴ نخ ، همزمان خواهد بود . در این باره (رابطه ی پروسه و نخ های اجرا شده ی در یک پروسه و همچنین نخ های اجرا شده توسط پروسه های مختلف) ، احتمالا در پست بعدی میگم."
این که گفته بودم ، اشتباست و در نظر نگیرید . قضیه و نکات کامل شو در همین پست میگم .
بعد اینکه هر هسته ی فیزیکی ، شامل حداقل یک هسته ی منطقی (مثل پردازنده ی Ryzen 3 1200) یا حداکثر دو هسته ی منطقی داره (که در این صورت همونطور که توضیح داده بودم ، تکنولوژی Hyper Thread در اینتل و به قول مهندس آرمین در AMD به عنوان SMT شناخته میشه و عموم مردم به عنوان پردازنده های چند نخی یا چند رشته ای ازش یاد میکنند مثل پردازنده ی Ryzen 5 3600 که 6 هسته ی فیزیکی که درون هر هسته ی فیزیکی اش ، 2 هسته ی منطقی که کلا میشه 12 هسته ی منطقی ، داره) .
در واقع ، اجرا کننده ی نهایِ نخ ها در تمام پردازنده ها ، هسته (های) منطقیِ اون پردازنده هست .
--------------------------------------------------------------------------------------------------------
از این به بعد ، آروم آروم روی بحث ارتباط نخ ها (که گفته بودم که نخ ها ، یه چیزِ نرم افزاری هستن) با پردازنده میپردازیم .
هر جا عبارت "پروسه" گفتم ، منظورم یک برنامه یا نرم افزار هست .
هر نخی که در هر پروسه ای ساخته میشه ، ممکنه درونِ هر هسته ی منطقیِ مجزای دیگه ای بصورت همزمان اجرا بشه . چه دو هسته ی منطقی ، داخل یک هسته ی فیزیکی باشن یا اینکه اون هسته ی منطقی ، داخل هسته ی فیزیکی مجزای دیگه باشه .
گفتم ممکنه ، چون همه چیز دست سیستم عامل هست . در واقع ، سیستم عامل ، بخشی تحت عنوان هسته ی مجازی داره که تقریبا میشه گفت کارش اینه که بگه کدوم نخ ، باید در کدوم هسته ی منطقی (در پردازنده) ، اجرا بشه . البته ، قطعا خود پردازنده هم روی اجرای نخ ها ، کنترل داره . یعنی فلان قدر کد را در فلان هسته ی منطقی اش اجرا میکنه و متوقف میکنه و بعد میره در فلان هسته ی منطقی اش ، ادامه ی اون نخ را اجرا میکنه که در ادامه ، این قضیه را توضیح میدم . معمولا اگه کدهای یه نخ ، اون قدر اون هسته ی فیزیکی را درگیر کنه (که در همون لحظه ، اون هسته ی منطقی ، بیکار نباشه) ، سیستم عامل ، نخ دیگه ای که در اون پروسه ساخته شد را به هسته ی منطقی دیگه ای برای اجرای همزمان میسپاره .
بنابراین ، اینکه یک نخ جدیدی که در یک پروسه ساخته شد ، درون یک هسته ی منطقی دیگه ای بصورت همزمان اجرا بشه یا نه ، بستگی به کد نویسی برنامه نویس داره .
ضمنا ، کلماتی مثل پردازش یا رندر کردن ، از دیدِ پردازنده ، میشه گفت کلمه ی درستی نیست (از دید خودمون را نمیگم) . پردازنده ، فقط اجرای کدهای اون نخ را انجام میده . بنابراین هر جا کلمه ی رندر یا پردازش میخوای در نظر بگیری ، برابر این بدون که اجرای کدهای یک نخ هست .
بریم سر وقت مثال تا در اونجا ، بهتر توضیح بدم :
کد:private void TransparentControl4_Click(object sender, EventArgs e) { Thread thread = new Thread(this.NewThreadMethod); thread.Start(); for (long i = 0; i < 10000000000; i++) { } MessageBox.Show("Main Thread Loop Finished"); } private void NewThreadMethod() { for (long counter = 0; counter < 10000000000; counter++) { } MessageBox.Show("New Thread Loop Finished"); }
اول اینکه ، کد بالا را توی هر پردازنده ای اجرا کنی ، چون دو نخ داره ، بنابراین در 2 هسته ی منطقی (چه هسته ی منطقی ، داخل یک هسته ی فیزیکی باشند یا نباشند) ، بصورت همزمان اجرا میشه (به عبارتی ساده تر ، بصورت همزمان ، در دو هسته اجرا میشه) .
البته نمیدونم وقتی که این کد را درون پردازنده ای که مثلا 4 هسته ی فیزیکی و 8 هسته ی منطقی (مثل Ryzen 2400G) اجرا کنیم ، دو نخ بالا ، در دو هسته ی منطقی ای که در یک هسته ی فیزیکی قرار دارند ، بصورت همزمان اجرا میشن (و بنابراین بخاطر حافظه ی مشترک این دو هسته ی منطقی که در یک هسته ی فیزیکی قرار دارند ، کاهش کارایی نسبت به هسته های فیزیکی مجزا داشته باشیم) یا اینکه بصورت همزمان درون دو هسته ی منطقی ای که هر کدوم شون درون دو هسته ی فیزیکی مجزا وجود دارند اجرا میشن (و بنابراین افزایش کارایی و سرعت اجرای کدشون بیشتر میشه) .
اما فکر کنم سیستم عامل و پردازنده اون قدر هوشمند هستند که در شرایطی که منابع پردازنده ، مشغول نباشه و بیکار باشه ، این کد را درون دو هسته ی منطقی ای که هر کدوم شون درون دو هسته ی فیزیکی مجزا وجود دارند اجرا کنه تا سرعت اجراشون بیشتر بشه .
شاید به نظرت بیاد که این کد بالا ، چیزی نداره و فقط یک حلقه ی خالی هست پس چرا این همه پردازنده را داره مشغول میکنه و این همه طول میکشه .
همونطور که میدونی ، حلقه ی for ، دستورش (که نیاز به پردازش داره) در دو جاست . اول ، قسمت آخرش یعنی "++i" . همونطور که میدونی ، این دستور ، برابر با دستور
هست . بنابراین ، دستور اول ، خوندن مقدار i هست . دستور دوم ، جمع کردنِ مقدار i (ای که خونده شده) با عدد 1 هست . و سومین عملی هم که پردازنده باید انجام بده اینه که نتیجه رو در حافظه ای (اول در رجیستری و بعد میتونه درون کش یا حتی رم و یا حتی حافظه ی مجازی درون هارد باشه) ذخیره کنه .کد:int i = 0; i = i + 1;
دستور بعدی هم که i < 10000000000 هست و مقایسه میکنه که مقدار i آیا کوچکتر از 10000000000 هست یا نه و نتیجه را در قالب عدد 0 یا 1 (در دیدِ ما به عنوان مقدار بولین یا همون bool) در نظر میگیره .
پس این حلقه ، در هر بار ، حداقل 4 دستور را اجرا میکنه .
همونطور که میدونی ، در کد بالا ، 2 نخ داریم .
یکی نخ اصلی که متد TransparentControl4_Click را اجرا میکنه . چون در زیر ، توضیحاتی مفصل ای میخوام بدم درباره ی اجرای این نخ ها و برای اینکه هر بار نگم فلان متد در فلان نخ ، بنابراین نخ ها را شماره گذاری میکنم . فرضا شماره ی این نخ را "نخ 1" میگیرم . بنابراین هر وقت گفتم نخ 1 ، منظورم همین نخ اصلی هست که متد TransparentControl4_Click توش قرار داره .
یکی هم نخ جدیدی هست که وقتی اجرا میشه که کد thread.Start که درون متد TransparentControl4_Click هست ، اجرا بشه . وقتی این کد اجرا بشه ، یه نخ جدیدی ساخته میشه که متد NewThreadMethod را در هسته ی منطقی جدید اجرا میکنه. شماره ی این نخ را "نخ 2" میگیریم (در واقع همون نخ ای هست که در متغییر thread ذخیره کردیم) .
اما میرسیم به این قضیه ی مهم که اجرای این نخ ها در پردازنده ، چجوری انجام میشه (پردازنده مون هم فرض میکنیم Ryzen 3 1200 هست که 4 هسته ی فیزیکی و 4 هسته ی منطقی داره) :
فعلا در نخ اصلی (نخ 1) قرار داریم و کنترل TransparentControl4 را کلیک کردیم و بنابراین رویداد (در واقع متد) TransparentControl4_Click مون میشه (هنوز نخ 2 در پروسه مون نداریم چون هنوز متد TransparentControl4_Click مون اجرا نشد چه برسه که کد thread.Start در این متد اجرا بشه و نخ جدیدی ساخته بشه) .
بعد از اجرای دو خط اول از متد TransparentControl4_Click و رسیدنِ به کد thread.Start ، پروسه مون ، درخواست اجرای نخ جدید (نخ 2) را به سیستم عامل میده و در نهایت ، سیستم عامل هست که نخ مون را برای اجرا ، به یه هسته ی منطقی میفرسته .
میدونی که سیستم عامل ، فقط مخصوص پروسه ی ما ساخته نشده . ممکنه در همین لحظه که پروسه ی ما درخواست اجرای این نخ 2 را به سیستم عامل میده ، پروسه های (برنامه های) دیگه هم خواستار اجرای نخ شون برای اجرا باشن . همه ی این نخ های درخواست شده ، توی صف ای در سیستم عامل قرار میگیرن تا سیستم عامل این نخ ها را برای اجرا ، برای پردازنده ارسال کنه . در این صورت ، این سئوال پیش میاد که سیستم عامل ، کدوم نخ (در تمام این پروسه ها) را زودتر برای اجرا برای پردازنده ارسال میکنه؟ یا در واقع ، توی این صف ، کدوم نخ ها ، جلوتر از بقیه قرار میگیرن؟
جوابش اینه که (همونطو در پست قبلی بهش اشاره کرده بودم) ، هر نخ ای که قبل از اجرا ، الویت بیشتری براش در نظر گرفته شده باشه ، سیستم عامل اون نخ را زودتر برای اجرا به پردازنده ارسال میکنه . این مهم هه که باید قبل از اجرای نخ (قبل از اینکه کد thread.Start را بنویسیم) ، الویت اون را مشخص کرد . الویت را با پروپرتی Priority در کلاس Thread مشخص میکنن . بنابراین اگه میخوایم در کد بالا ، برای نخ 2 مون ، الویت مشخص کنیم ، مینویسیم :
که الویتِ Highest ، بالاترین الویت را در نظرمیگیره . این به این معناست که اگه همزمان مثلا 100 نخ (از هر پروسه ای) ، به سیستم عامل ، درخواست اجرا بدن ، نخ 2 مون و همچنین همه ی نخ هایی که این الویت را دارند ، سیستم عامل این نخ ها را زودتر از بقیه ی نخ ها ، برای اجرا ، به پردازنده ارسال میکنه .کد:Thread thread = new Thread(this.NewThreadMethod); thread.Priority = ThreadPriority.Highest; thread.Start();
اما اگه در همون لحظه ، فقط این نخ (نخ 2 ام) وجود داشت و هیچ نخ دیگه ای وجود نداشت ، بنابراین تعیین الویت ، هیچ فایده ای برای نخ ام نداره چون فقط خودش هست و سیستم عامل این نخ را برای اجرا به پردازنده میفرسته .
بعد از درخواست اجرای نخ دوم (و قضیه ای که گفتم) ، حالا سیستم عامل ، نخ 2 ام را به هسته ی منطقی دیگه ای برای اجرا ، ارسال میکنه (همونطور که گفتم ، به احتمال بسیار زیاد ، سعی میکنه این نخ 2 را ، اگه هسته ی منطقی ای که در هسته ی فیزیکیِ دیگه بیکار هست ، بفرسته چون سرعت اجراش بیشتر از دو هسته ی منطقی در یک هسته ی فیزیکی هست) . این در حالی هست که نخ 1 (نخ اصلی ام) ، در هسته ی منطقی دیگه ای در حال اجراست .
پروسه ی ما که الان 2 نخ داره . فرض میکنیم همزمان با پروسه ی ما ، 3 نخِ دیگه از پروسه ها و برنامه های دیگه (مثلا برنامه ی padvish و idm و after effect باشن) رو هم سیستم عامل برای پردازش ، به پردازنده میفرسته .
اما آیا یه نخ ای که برای اجرا ، به سیستم عامل درخواست داده بود ، سیستم عامل ، کل کدهای اون نخ را برای اجرا ، به پردازنده میفرسته؟ یعنی در کد ما ، همه ی کدهای نخ 2 ، یعنی همه ی بدنه ی متد NewThreadMethod ، یک دفعه ، همه ی کدهاش به یکباره اجرا میشه؟
جواب ، منفی هست . کل یک نخ ، به یکباره اجرا نمیشه .
سیستم عامل ، قسمتی از یک نخ را برای اجرا به یک هسته ی منطقی میفرسته ، اون هسته ی منطقی ، اون کد را اجرا میکنه . بعد ، سیستم عامل ، بخش دیگه از یک نخ دیگه (اون نخ میتونه برای یک پروسه یا از پروسه های دیگه باشه) را برای اون هسته ی منطقی (یا هسته ی منطقیِ دیگه) میفرسته ، اون هسته ی منطقی ، اون کد را اجرا میکنه و این روند ادامه داره .
در واقع ، سیستم عامل از هر نخ ، فقط بخشی از کدش را برای پردازش ، به پردازنده میفرسته نه همه ی کدهای اون نخ را .
بنابراین ، هر هسته ی منطقی ، فقط بخشی از کدهای یک نخ را پردازش میکنه نه همه اش را .
البته نمیدونم این کار (بخش بخش کردنِ کدهای یک نخ برای اجرا و پردازش) ، کارِ سیستم عامل هست یا کارِ خود پردازنده یا هر دوشون .
یعنی مثلا نمیدونم خود سیستم عامل ، کل نخ را برای اون هسته ی منطقی میفرسته اما اون هسته ی منطقی ، کدهای یک نخ را تقسیم میکنه و بخشی اش را اجرا میکنه و بعد میره سراغِ نخِ دیگه تا بخشی از کدهای اون نخ را اجرا کنه و دوباره میاد سراغِ این نخ تا ادامه ی کدهاشو اجرا کنه و این روند همینطور ادامه داره .
ولی مهم ، نتیجه هست . و نتیجه هم اینه که هر هسته ی منطقی ، بخشی از کدهای یک نخ را اجرا میکنه و بعد متوقف اش میکنه و میره سراغ اجرای کد در نخ دیگه ای .
که به این توقف دادنِ به اجرای یک نخ و رفتنِ به سراغِ نخ دیگه (برای اجرای کدهاش) ، سوئیچ کردن (اون هسته ی منطقی) یا Context Switch میگن .
قبل از اجرای یک نخ در هر هسته ی منطقی ، اگه قراره نخِ جدیدی اجرا بشه (مثل همین نخ شماره ی 2 مون) ، باید حافظه ی جدیدی برای اون نخ در رجیستر در نظر گرفته بشه تا اطلاعات وضعیت نخ در اون ثبت بشه . هسته ی منطقی ، بخشی از کدِ اون نخ را اجرا میکنه و بعد ، اطلاعاتی که مشخص میکنه تا کجای این نخ را اجرا کرد (این اطلاعات ، احتمالا آدرس حافظه ی کد یا نخ و متدی که تا اونجا ، اون کد را اجرا کرد ، هست) را توی حافظه ی رجیستری اش ذخیره میکنه . این اطلاعات ، بعدا وقتی ادامه ی این نخ ، درون هر هسته ی منطقیِ دیگه ای اجرا بشه ، به درد اون هسته ی منطقی میخوره .
این اطلاعات نخ ، در صورتی که حافظه ی رجیستری ، فضای کافی برای ذخیره نداشته باشه ، به حافظه ی کش l1 و اگه اون هم فضا نداشته باشه ، به کش l2 و همینطور اگه اون هم فضا نداشته باشه به حافظه ی کش l3 و اگه اون هم فضا نداشته باشه به حافظه ی اصلی (یا همون رم) و اگه اون هم فضا نداشته باشه به حافظه ی مجازی که در هارد قرار داره ممکنه منتقل بشه .
اما اگه قراره ادامه ی یک نخ اجرا بشه (یعنی قبلا ، بخشی از کد اون نخ ، اجرا شده بود)، وضعیت هر نخ ، در حافظه ی رجیستریِ اون هسته ی منطقی باید بارگذاری بشه . حالا وضعیت اون نخ ممکنه در خودِ حافظه ی رجیستر بوده باشه که در این صورت ، پردازنده منتظر نمیمونه وگرنه باید از حافظه ای که ممکنه در کش (سطح l1 یا l2 یا l3) یا حافظه ی اصلی رم یا حافظه ی مجازی هارد دیسک بوده باشه ، اطلاعات این وضعیتِ نخ ، به حافظه ی رجیستری منتقل بشه که در این صورت ، بخاطر سرعت بسیار پایینِ بقیه ی حافظه ها ، اون هسته ی منطقی ، مجبور هست که منتظر رسیدن اطلاعات نخ جدید از اون حافظه ، به حافظه ی رجیستر باشه یا اینکه به نخ دیگه ای سوئیچ کنه (Context Switch) و کدهای اون را اجرا کنه تا اطلاعات این نخ برسه .
بنابراین ، فرض میکنیم نخ 1 مون (نخ اصلی) در حال اجرا شدن در هسته ی (منطقی) شماره ی 0 هست (یادت باشه ، پردازنده مون Ryzen 1200 بود) . وقتی کد thread.Start اجرا بشه ، (ممکنه) نخ 2 مون در یک هسته ی منطقی دیگه ، مثلا در هسته ی (منطقی) شماره ی 1 اجرا بشه (بستگی به این داره که هسته های منطقی دیگه در اون لحظه بیکار باشن یا نه و همینطور به نوع کد ما هم بستگی داره که قبلا توضیح دادم . حالا ما میگیم در هسته ی 1 اجرا میشه) . در همین زمان ، دو نخ از اون 3 نخ (برای اون 3 برنامه) هم همزمان در هسته های منطقیِ دیگه دارن اجرا میشن . فرض میکنیم نخ برنامه ی padvish در هسته ی منطقی 2 و نخ برنامه ی idm هم در هسته ی منطقی 3 داره اجرا میشه .
هسته ی منطقی 1 ، بخشی از کد نخ 2 مون را اجرا میکنه . بنابراین هسته ی 1 ، بخشی از کد متد NewThreadMethod ، که کلا بدنه ی اصلی اش ، 10000000000 (ده میلیارد) بار تکرار حلقه هست (مثلا 50 تای اولش) را اجرا میکنه .
باز هم تاکید میکنم که این در حالی هست که در همین زمان (که هسته ی 1 داره 50 بار از حلقه در این متد در نخ 2 مون را اجرا میکنه) ، هسته ی (منطقی) شماره ی 0 هم داره ادامه ی کدهای نخ 1 (یا همون نخ اصلی مون که همون ادامه ی متد TransparentControl4_Click ، یا به عبارتی از بعد از کد thread.Start که به کدهای حلقه داخل متد TransparentControl4_Click میرسه) را اجرا میکنه . فرض میکنیم که هسته ی 0 ، مثلا 100 تای اول از حلقه ی داخل متد TransparentControl4_Click (که در نخ 1 قرار داره) را داره اجرا میکنه .
اطلاعات نخ را در رجیستری اش ذخیره میکنه و میره سراغِ اجرای نخ بعدی ؛ که این نخِ (بعدی) ، میتونه یک نخ از پروسه ی دیگه باشه یا نخ دیگه ای در پروسه ی خودمون . که در اینجا ، در واقع همون سوئیچ (یا Context Switch ای که قبلا گفته بودم) رخ میده .
این را هم بگم ، اینکه یک هسته ی منطقی (حتی در شرایط و تعداد حلقه های برابر که دو هسته ی منطقی دارن با هم انجام میدن) ، تا کدوم بخش از کد را انجام میده ، معلوم نیست و دست خودشه . اینکه مثلا هسته 0 و هسته ی 1 ، هر دو دارن یک تعداد حلقه را اجرا میکنن پس باید هر دو تا ، 50 تای اول را اجرا کنن ، این طور نیست . هسته ی 0 میتونه 100 تا را اجرا کنه و هسته ی 1 میتونه 50 تا را . یا هر تعداد که خودشون میدونن که در ادامه توضیح میدم .
این نکته را هم در نظر بگیر که هر سوئیچ ای که (برای نخ) در هسته ی منطقی اتفاق میافته ، برای اون هسته ، سربار داره . به این معنی که اول باید وضعیت اون نخ را در حافظه اش ذخیره کنه و دوم اینکه وضعیت نخ جدیدی که میخواد اجراش کنه را اگه توی حافظه ی رجیستر وجود نداشته باشه ، باید توی حافظه ی رجیستر لود کنه (و بسته به اینکه در کدوم حافظه ی کش یا رم یا حافظه ی مجازی ذخیره شده باشه باید به همون نسبت ، بیشتر منتظر بمونه) و همچنین بقیه ی عملیات مربوط به سوئیچِ نخ را باید انجام بده که در کل باعث میشه هر سوئیچ نخ ای انتظار و سربار خودش را داشته باشه . بنابراین معمولا اون هسته ی منطقی زمانی سوئیچ را انجام میده که ارزش این سربار را داشته باشه .
مثلا وقتی 2 بار حلقه ی ما را اجرا کنه و 10 میکروثانیه زمان ببره اما سوئیچ به نخ بعدی ، برای اون هسته ی منطقی ، 50 میکروثانیه زمان لازم داشته باشه ، قطعا نمیارزه در این حالت ، سوئیچ انجام بده . بنابراین میاد 50 بار حلقه ی ما را اجرا میکنه و 250 میکروثانیه زمان میبره و یه سوئیچ (به نخ دیگه ای) انجام میده و 50 میکروثانیه ازش زمان میبره تا ارزش سوئیچ کردن را داشته باشه .
واسه ی همینه (واسه ی قضیه ی سربار هست) که اجرای نخ های خیلی کوچیک ، وقت اون هسته ی منطقی را بیشتر میگیره و اون هسته ی منطقی، عملا بجای اینکه به درد اجرای کد بخوره ، بیشتر داره زمان برای سوئیچ روی نخ هاش صرف میکنه . مثلا در نظر بگیر 1000 تا نخ داری که توی هر کدوم شون (از توابع ای که در اون نخ ها نوشتی) ، 2 تا کار داری انجام میدی (که در مجموع میشه 2000 کار) و همه ی این نخ ها (این 1000 تا نخ) را با هم اجرا میکنی و در مقابل یک نخ داری که همه ی این 2000 تا کار را توی یک حلقه انجام میدی . اجرای این یک نخ ای که 2000 تا کار را توی یه حلقه انجام میده ، زودتر انجام میشه نسبت به اون 1000 نخ ای که باز هم همین کار ها را انجام میده . بخاطر همین سربار .
در واقع ، مهمترین عامل بهتر کار کردن دو برنامه و نرم افزار نسبت به هم ، نوع کدنویسی برنامه نویس هاست که چجوری کد بنویسن که از یه پردازنده ، بهتر بهره ببرن .
یادت باشه ، سوئیچ نخ ، بین دو هسته ی منطقی ای که توی یک هسته ی فیزیکی قرار دارن (پردازنده های HT یا SMT) ، سربار کمتری نسبت به سوئیچ نخ بین دو هسته ی منطقی ای که درون هسته های فیزیکی جداگانه وجود دارن ، داره .
خوب دقت کن که از اینجا به بعد ، مباحث به هم ربط داره و گره میخوره .
خوب ، تا اینجا را گفته بودم که هسته ی منطقی 1 مون ، 50 بار از حلقه ی for (در متد NewThreadMethod در نخ 2 مون) را اجرا کرد (در همین لحظه ، هسته ی منطقی 0 ، در حال اجرای 100 بار از حلقه در نخ 1 هست) و هسته ی منطقی 1 ، حالا اطلاعات نخ 2 مون را در رجیستری ذخیره کرد و به نخ دیگری که میتونه هر نخی از هر پروسه ای باشه (حتی نخ دیگه ای از پروسه ی خودمون باشه)، سوئیچ داره میکنه (تا بخشی از کدهای اون نخ را انجام بده) . فرض میکنیم هسته ی 1 مون داره به نخ ای از برنامه ی after effect سوئیچ میکنه (تازه برای اولین بار ، نوبت اجرای نخ برنامه ی after effect شد) .
هسته ی 1 مون ، شروع به اجرای بخشی از کدهای نخ در برنامه ی after effect میکنه . اجرای این بخش از کد از این برنامه در این هسته را فرض میکنیم 20 میلی ثانیه طول بکشه . وقتی که هسته ی 1 مون ، 10 میلی ثانیه از اجراش گذشت (یعنی نصفِ بخشی از اون کد در این برنامه را انجام داد) ، در این زمان هسته ی 3 مون ، نخ برنامه ی idm را اجرا کرده بود را متوقف کرد و سراغ ادامه ی کدهای نخ 2 در برنامه ی ما رفت .
خوب ، برای ادامه ی اجرای یک نخ توسط هسته ی منطقی ، نیاز داره تا به اطلاعات اون نخ که ذخیره شده بود ، دسترسی پیدا کنه . نخ 2 مون ، آخرین بار در کدوم هسته ی منطقی اجرا شده بود؟
در هسته ی شماره ی 1 .
پس اطلاعاتش هم در هسته ی فیزیکی شماره ی 1 قرار داره . پس اطلاعاتش از رجیستری در هسته ی فیزیکی شماره ی 1 باید به حافظه ی رجیستری در هسته ی فیزیکی شماره ی 3 منتقل باید بشه . برای این انتقال ، چاره ای نیست تا اطلاعات ، اول به حافظه ی مشترک بین همه ی هسته های فیزیکی که همون حافظه ی کش هست (حالا نمیدونم سطح l1 یا l2 یا l3 . قطعا کش l3 که مشترک هست) منتقل بشه و بعد در حافظه ی رجیستری در هسته ی فیزیکی شماره ی 3 منتقل بشه .
و چون سرعت حافظه ی کش ، خیلی کندتر از حافظه ی رجیستری هست ، به همون نسبت این سربار و انتظار ، بیشتر طول میکشه .
بنابراین به یاد داشته باش که سربارِ سوئیچ بین دو نخ ای که در دو هسته ی فیزیکی مجزا اتفاق میافته ، خیلی بیشتر از سوئیچ نخ ها در هسته یا هسته های منطقی ای که در یک هسته ی فیزیکی اتفاق میافته ، هست .
اما این در بهترین حالت هست . چون در بهترین حالت ، اطلاعات نخ ها در حافظه ی رجیستری وجود داشت اما اگه در حافظه های دیگه مثل کش یا مخصوصا حافظه ی رم و ... وجود داشته باشه ، به همون نسبت انتظار بیشتر میشه که گفته بودم .
به این ترتیب ، ادامه ی نخ 2 ما ، یعنی از ادامه ی حلقه ی مون (حلقه ای که در متد NewThreadMethod بود) ، یعنی از دفعه ی 51 امین بار به بعد در حلقه مون ، در هسته ی منطقی شماره ی 3 اجرا میشه . این بار مثلا 300 بار کد را اجرا میکنه (که کلا با 50 دقعه ی قبلی که هسته ی قبلی اجرا کرده بود ، کلا 350 بار اجرا بشه) . تازه شروع به اجرا میکنه .
در همین لحظه ، هسته ی 0 ، اون 100 بار اجرایی که حلقه ی مون در نخ اصلی (نخ 1) را قبلا شروع کرده بود را تمام کرد و اجرای نخ اصلی (نخ 1) از برنامه مون را متوقف کرد . (و همچنین کارهای مربوطه مثل ثبت وضعیت نخ 1 مون و ... را انجام داد) و در همین لحظه ، سیستم عامل ، یه نخ دیگه از یک برنامه ی جدید (مثلا برنامه word) را بهش برای اجرا سپرد را میخواد اجرا کنه .
این چرخه ی توقف و اجرای نخ ها توسط هسته های منطقی ، این قدر ادامه داره که اون نخ تمام بشه .
فرض میکنیم که اجرای حلقه ی for در نخ 2 مون تمام شد . پس سرغ ادامه ی کد یعنی متد MessageBox.Show میره و همین چرخه ، براش اتفاق میافته (یعنی ممکنه بخشی از بدنه ی این متد را اجرا و بعد متوقف کنه و همین داستانی که توضیح دادم) . بعد هم فرض میکنیم که اجرای بدنه ی متد MessageBox.Show را هم تمام کرد و پیام "New Thread Loop Finished" بهمون نمایش داده شد . و چون این متد ، در هر نخ ای که اجرا بشه ، باعث میشه اون نخ متوقف بشه تا اینکه کاربر نهایی ، به پنجره ی پیامی که داده شد ، جواب بده (و یکی از دکمه هاش را کلیک کنه) ، پس تا اون زمان ، هنوز نخ 2 تمام نخواهد شد .
چون حافظه ی رجیستری بسیار محدود هه و جواب دادن کاربر هم پروسه ی طولانی برای پردازنده محسوب میشه (حتی اگه کاربر ظرف 100 میلی ثانیه جواب بده) ، اطلاعات نخ ، احتمالا به حافظه ی رم منتقل میشه .
وقتی که کاربر جواب این پیام را داد ، ادامه ی کد در نخ 2 اجرا میشه . یعنی در واقع اجرای متد NewThreadMethod تمام میشه (چون کد دیگه ای نداره . وقتی اجرای متد تمام بشه ، حافظه ای که برای متد در نظر گرفته شده بود ، پاک میشه) و تمام شدنِ این متد هم به منزله ی تمام شدنِ نخ 2 مون هست . بنابراین در این صورت ، حافظه ای که برای نخ 2 مون در نظر گرفته شده بود (و اطلاعات مربوط به این نخ) ، هم از حافظه پاک میشه .
بعد هم چرخه ای که گفتم ، برای نخ اصلی مون (نخ 1) این قدر ادامه داشت که هم حلقه ی داخلِ این نخ که در حال اجرا بود (در متد TransparentControl4_Click) ، تمام شد و هم کد بعدش یعنی متد MessageBox.Show اجرا شد و پیام "Main Thread Loop Finished" به کاربر نمایش داده شد و کاربر هم به این پیام جواب داد . آیا با جواب دادن کاربر به این پیام ، نخ اصلی مون هم (مثل نخ 2 که تمام شده بود) تمام میشه ؟
جواب منفی هست .
چون در متد main ، کنترل فرم مون را با کد Application.Run اجرا کردیم و نمایش دادیم و فرم ای که به این صورت اجرا بشه ، با اتمام اجرای متد در نخ اصلی ، اون نخ اش بسته نمیشه . مگر اینکه به اون فرم ، پیام close فرستاده بشه یا به عبارتی ساده تر ، کاربر دکمه ی close رو در فرم کلیک کنه (یا کد بستن فرم نوشته بشه) .
حالا اگه نخ پروسه مون که در این مثال 2 تا بود ، بجاش 100 تا هم باشه ، باز هم مثل همین چرخه اتفاق میافته (عملیات سوئیچ و چیزهایی که توضیح دادم)
کدی که دادم را اگه اجرا کنی ، برنامه ات موقتا not responsible میشه . چون تا کدهای یک نخ تمام نشد ، اون نخ پاسخگو نخواهد بود و چون حلقه ی for در متد TransparentControl4_Click اگه اجرا بشه ، به این زودی ها تمام نمیشه (حداقل بیش از 20 ثانیه اجراش طول میکشه) و چون این متد هم در نخ اصلی و نخ اصلی هم که کنترل فرم ما توش اجرا میشه ، پس کنترل فرم ما با اجرای این حلقه درون متد TransparentControl4_Click ، بصورت not responsible درمیاد . برای حل این مشکل ، مشخص هست که کد حلقه در متد TransparentControl4_Click را هم باید باز در نخ جدید (یک نخ جدید دیگه) اجرا کنیم یعنی این جوری بنویسیم :
کد:private void TransparentControl4_Click(object sender, EventArgs e) { Thread thread = new Thread(this.NewThreadMethod); thread.Start(); Thread thread_3 = new Thread(this.OtherThreadMethod_3); thread_3.Start(); } private void NewThreadMethod() { for (long counter = 0; counter < 10000000000; counter++) { } MessageBox.Show("New Thread Loop Finished"); } private void OtherThreadMethod_3() { for (long counter = 0; counter < 10000000000; counter++) { } MessageBox.Show("Other Thread Loop Finished"); }
یا اگه دو نخ ، دقیقا یک کار را انجام میدن ، میتونی آرایه ای از نخ ها بگیری :
کد:private void TransparentControl4_Click(object sender, EventArgs e) { Thread[] thread = new Thread[2]; for (int i = 0; i < thread.Length; i++) { thread[i] = new Thread(this.NewThreadMethod); thread[i].Start(); } } private void NewThreadMethod() { for (long counter = 0; counter < 10000000000; counter++) { } MessageBox.Show("New Thread Loop Finished"); }
دقت کن ، مثلا در این کد بالا که تعداد آرایه 2 هست و یعنی 2 نخ بصورت همزمان در هسته های منطقی مجزا اجرا میشه (البته با کدهای نخ اصلی که میشه 3 تا . اما نخ اصلی که کد خاصی برای اجرا نداره . یعنی نیاز به پردازش و اجرای طولانی نداره) (البته خود clr هم نخ های دیگه ای را برای پروسه مون در نظر میگیره که اونها را فعلا در نظر نمیگیریم) ، این 2 نخ را درون یک پردازنده ای که 2 هسته ی فیزیکی که هر هسته ی فیزیکی اش 1 هسته ی منطقی داره (کلا پردازنده ی دارای 2 هسته ی فیزیکی و 2 هسته ی منطقی) اجرا کنی و هم اینکه در پردازنده ای که مثلا 32 هسته ی فیزیکی و 64 هسته ی منطقی داره اجرا کنی ، فرق چندانی توی زمان اجراشون ندارن .
چون این کد که 2 نخ داره ، پس نیاز به 2 هسته ی منطقی (اگه داخل هسته های فیزیکی مجزا باشن ، بهتره) برای اجرا داره . که هر دوی این پردازنده ها ، 2 هسته ی فیزیکی دارن . در واقع 30 هسته ی فیزیکی از پردازنده ای که 32 هسته ی فیزیکی داشت ، بیکار هه .
بنابراین تفاوت پردازنده ها ، زمانی مشخص میشه که بصورت همزمان ، حداقل به تعداد هسته های منطقی اون پردازنده ای که بیشترین هسته های منطقی را داره (در اینجا پردازنده ای که 32 هسته ی فیزیکی و 64 هسته ی منطقی داره) ، نخ (نخ ای که همزمان اجرا بشه) داشته باشه . یعنی تفاوت در این پردازنده ها را زمانی متوجه میشی که 64 نخ را بصورت همزمان در هر دوی این پردازنده ها اجرا کنی (یعنی تعداد اون آرایه را برابر با 64 بگیری) .
این را در هم یادت باشه که هسته های منطقی ، بصورت همزمان ، فقط به تعداد کانال های اون رم میتونن درخواست خوندن یا نوشتن بدن . مثلا مادربردهای خونگی ، معمولا رم های دو کاناله دارن . به این معنی هه که همزمان 2 هسته ی منطقی (اون هم هر کدوم از دو کانال مجزای رم) ، میتونه درخواست خوندن و نوشتن به رم رو کنه . یعنی مثلا اگه پردازنده ای 8 هسته ی فیزیکی و 16 هسته ی منطقی داشته باشه ، همزمان ، فقط دو هسته ی منطقی از این 16 هسته ی منطقی (اون هم هر کدوم از دو کانال متفاوت) میتونن درخواست خوندن یا نوشتن اطلاعات را از رم بدن .
ممکنه برات سئوال پیش بیاد که همه ی اطلاعات که در رم ذخیره میشه . پس یعنی 14 هسته ی منطقیِ دیگه در این پردازنده ، در همون زمان بیکارن و هیچ کار دیگه ای انجام نمیدن؟
نه این طور نیست . اولا همه ی اطلاعات در رم ذخیره نمیشه . اصلا حافظه ی رم ، برای پردازنده بسیار بسیار بسیار کند هه . واسه ی همین ، حافظه ی کش رو ساختن . اون هم در 3 سطح (که میدونی سرعت هر سطح ، تفاوت بسیار زیادی با سطح قبلی از حافظه ی کش داره) (که اطلاعات مهمتر و اطلاعاتی که دم دست نیاز داره ، از حافظه ی رم به حافظه ی کش منتقل میشه) . سرعت کش (هر 3 سطح) هم برای پردازنده بازم بسیار کند هه و حافظه ی اصلی اش که اون رو منتظر نذاره (و حافظه ی اصلی در پردازنده هست) ، حافظه ی رجیستری هست . بنابراین ، پردازنده نسبت به اینکه به حافظه ی رجیستری و کش اش ارتباط داره ، خیلی کمتر از اینها با حافظه ی رم ارتباط داره (متوجه ی منظورم که شدی ، یه وقت اشتباه برداشت نکنی) .
منظورم این نیست که ارتباطش با حافظه ی رم ، کم هه . منظورم نسبی بود .
اگه نیاز به حافظه رم داشته باشه ، اطلاعاتی که اون هسته ی منطقی درخواست کرد را تا از رم به کش و رجیستر برسه ، به نخ های دیگه سوئیچ میکنه (البته ممکنه . چون ممکنه نخ دیگه ای در اون لحظه برای اجرا وجود نداشته باشه و دیگه انتظار ، حتمی هه) و اونها را اجرا میکنه .
این رو هم یادت باشه که وقتی یه هسته ی منطقی میخواد یه کد را اجرا کنه ، با کلاک و فرکانسی که براش در نظر گرفته شد ، این کار را میکنه . مثلا اگه فرکانسش 3.4GHZ هست ، با تمام توان اون فرکانس اجراش میکنه . نمیاد مثلا نصف اش کنه مثلا از این 3.4 گیگاهرتز ، 2 گیگاهرتز را روی این نخ میذارم برای پردازش و در همین زمان ، بقیه ی فرکانس اش را برای نخ دیگه ای برای پردازش میذارم . قضیه ی پردازش و اجرای نخ را که در همین پست توضیح دادم (اینو گفتم چون در سئوالاتت بود)
در پست بعدی ، به کدهای دیگه ی چند نخی میپردازیم .
Bookmarks