PDA

مشاهده نسخه کامل : رابطه بین نوع برنامه نویسی نرم افزار و عمل پردازنده ها



Speed Racer
16-11-19, 10:13
درود

س1) از کجا بفهمیم که یک نرم افزار از یک ترد استفاده می کنه یا دو ترد به صور همزمان. توی نرم افزارهای بزرگ این مورد نوشته می شه مثل Cinem4D ولی برای خیلی ها نوشته نمی شه
س2) فرض کنید یک پردازنده 6 هسته ای دارم با فرکانس 3 گیگاهرتز و 12 ترد. حالا می خوام چند تا برنامه به طور همزمان باز کنم و این برنامه ها به فرض رندر می کنن. حالا این نرم افزارها و عمل رندر به کدوم یک از روش هایی که میگم انجام می شه. این حالت ها برای نرم افزارهای 1 تردی یا دو تردی فرق دارن


الف) نرم افزار اول باز شده و در حال رندر کردنه و 2 هسته از سی پی یو رو اشغال می کنه و از هر هسته فقط 2 گیگاهرتز اشغال می کنه. بعد نرم افزار دوم رو باز می کنم و اون هم به دو هسته نیاز داره و چون از هر یک از دو هسته اول یک گیگاهرتز باقی مونده اول اون دو تا یک گیگاهرتز از هتسه اول رو اشغال می کنه و باقی پردازش میره روی هسته های 3 و 4 و همینطور که نرم افزارهای دیگه رو باز می کنم هسته های بعدی درگیر میشن تا 6 هسته پر بشه و 100 درصد پردازنده اشغال بشه


ب) نرم افزار اول باز شده و در حال رندر کردنه و پردازش روی 6 هسته پخش می شه و همینطور که نرم افزارهای بعدی که باز می کنم پردازش اونا هم روی این 6 هسته پخش می شه و به طور کلی این 6 هسته کم کم پر می شه تا 100 درصد پردازنده اشغال بشه.


س3) ما برنامه نویسی موازی داریم. این برنامه نویسی موازی در نرم افزار باعث می شه که عمل رندر با دو سی پی یو همزمان انجام بشه یا اگه سی پی یو 8 هسته ای داشته باشیم روی هر 8 هسته انجام می شه که شاید دیگه نشه بگیم موازی چون به جای دو هسته داره روی 8 هسته انجام می شه.


س4) الان بیشتر پردازنده های اینتل مثلا 6 هسته و 6 ترد دارن ولی AMD 6 هسته و 12 ترد داره. و من در جایی خوندم که هر کدوم مزایا و معایبی دارن. بعضی جاها این دو ترد داشتن مهمه و بعضی جا ها تک ترد بودن. می خوم کمی در این رابطه توضیح بدی. اصلا اگه دو ترد داشتن خیلی خوبه چرا اینتل گیر داده به تک ترد روی بیشتر پردازنده هاش.

س5) پردازش در سی پی یو ها به صورت صف عمل می کنه یا پشته.


س6) خیلی از ویژگی هایی که در پردازنده های اینتل وجود داره در پردازنده های AMD نیست یعنی کاربردی ندارن. (منظورم از ویژگی ها مثلا SSE4.2 هست و امثال این ها )



س7) اگه معماری پردازنده ها رو در نظر بگیریم چه فرقی بین نسل 3000 رایزن و نسل 9 اینتل وجود داره. عمل پردازش اینا به یک شکله یا هر کدوم به شیوه خودشون پردازش می کنن. چون در خیلی جاها من دیدم که اینتل با تعداد هسته کمتر قدرتی تقریبا برابر AMD داره( به طور کلی میگم). کلا یک مقایسه ای وجود داره که بفهمیم معماری که اینتل در پردازنده هاش داره بهتره یا AMD در آخرین نسل.

اگه هر توضیح دیگه ای دارین که به روشن شدن مطلب کمک می کنه یا هر اطلاعات دیگه ای ممنون میشم که راهنمایی کنید.

SajjadKhati
22-11-19, 15:58
سلام بر گلپسر عزیز .
خیلی سئوال خوبی پرسیدی . اما اول بذار قبل از جواب دادن به سئوالاتت بصورت مجزا ، یه توضیح مفصل از عملکرد پردازنده ها و رابطه اش با نرم افزارها را بگم چون هر کدوم از سئوالاتت به اون یکی ربط داره و جوابش بصورت یک سریال هه . شاید این توضیح خودش یه پست کامل بشه . بعد میریم سر وقت جواب دادن به دونه دونه ی سئوالاتت . چون اینترنت بین الملل الان کار نمیکنه ، نمیشه بعضی از لینک های مفید را شاید داد .




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

اما یه برنامه (یا یک پروسه) ، میتونه بیشتر از یک نخ داشته باشه . برنامه نویس میتونه برای برنامه و پروسه اش ، تعداد بی نهایت نخ درست کنه (البته منظورم از بی نهایت ، به معنای واقعی کلمه و اجرا و ایجاد یک دفعه ی تعداد بسیار زیادی از نخ ها نیست چون کامپیوترها محدودیت منابع و حافظه دارن) . همانطور که شما میتونی تعداد بی نهایت متد را اجرا کنی چون در هر تعداد اجرای متدها که مشکلی نداری (البته این هم مثل اون ، نه به معنای واقعی کلمه و اجرای متدهای تو در تو بصورت بسیار زیاد نیست چون مثل همون قضیه ، هر وقت متدی اجرا میشه ، نیاز به حافظه ی جدیدی داره و کامپیوتر هم منابع و حافظه ی محدودی دارن و ارور stack overflow را میده که در قسمت 25 و 26 در آموزش سی شارپ ، توابع بازگشتی و اجرای تو در توی متدها ، گفته شد) .

دقت کن منظورم از اینکه گفتم "نخ هم چیزی نیست جز اجرای تابع یا متد" ، این نیست که هر وقت متدی را فراخونی کردی ، پس یعنی نخ جدیدی را ایجاد و فراخونی کردی . نه . فرایند ساخت و اجرای نخ ، متفاوت هه . باید از کلاس Thread یا ThreadPool یا Task یا BackgroundWorker (یا کلاس های بسیار زیادی که در این رابطه وجود دارن) ، شی بسازید و متدی را به این کلاس معرفی کنید و توسط اعضای این کلاس ، اون متد را فراخونی کنید تا در نخ جدیدی ، متدتون فراخونی بشه .


همونطور هم که هر وقت و در هر رویدادی که خواستی ، یه متد (یا متدهایی بصورت پشت سر هم یا حتی تو در تو) را فراخونی کنی و بعد اجرای اون متد (یا متدها) تمام میشه و در زمان و رویداد دیگه ای ، میتونی یک متد (یا متدهایی بصورت پشت سر هم یا حتی تو در تو) فراخونی کنی ؛ نخ ها هم به همین صورت ، هر وقت برنامه نویس خواست ، یه نخ (یا نخ های متوالی) را میتونه اجراشون کنه و بعد اجرای اون نخ (یا نخ ها) تموم میشه و در زمان و رویداد دیگه ای ، میشه باز یک نخ (یا نخ هایی را بصورت متوالی) اجرا کرد .
بنابراین اجرای نخ در یک پروسه (برنامه) ، میتونه مدام در حال تغییر باشه چون همونطور که گفتم ، ممکنه نخ هایی را در یک زمان (و رویداد خاصی) ایجاد و اجرا کرده باشن و اجرای نخ (و بنابراین خود اون نخ) تموم بشه (یا حتی برنامه نویس اون نخ را متوقف کنه تا اجرای اون نخ تموم نشه) و چند مدت بعد ، دوباره در یک رویداد (یا متد دیگه) ، نخ (یا نخ های جدید) دیگه ای را ایجاد و اجرا کرده باشن . بنابراین مثلا یه پروسه ، میتونه حتی ده ها نخ را در یک زمان خاص داشته باشه (مثلا 100 تا نخ) اما در ثانیه ی بعد ، همه ی اجرای اون نخ ها تموم شده باشه و فقط یک نخ براش مونده باشه اما باز دوباره در لحظه ی بعد ، مثلا 15 نخ را اجرا کنه اما باز دوباره در لحظه ی بعد ، مثلا اجرای 10 نخ اش تمام شده باشه و بنابراین 5 نخ اش مونده باشه . میخوام بگم که این طور نیست که حتما یک پروسه مثلا 3 نخ را از اول اجرای برنامه اش که اجرا کرد ، تا آخرِ اجرایِ برنامه و پروسه اش ثابت باشه و دیگه اجرای نخ اش تموم نشه یا دیگه اضافه نشه . هر لحظه ممکنه اجراشون تموم بشه (و بنابراین نخ از بین بره) و هر لحظه هم ممکنه نخ جدید بسازه و اجرا کنه . هر چند ، میتونه هم در مواقعی نخ را متوقف کنه و کاری کنه تا آخر اجرای پروسه ، نخ های دیگه ای که ساخت ، تمام نشه و در حال اجرا باشه .


حالا چرا اصلا نخ دیگه و نخ جدید ساخته میشه (و چرا در همون نخ اصلی همه ی کارها انجام نمیشه) ، بخاطر اینه که گاها پیش میاد یه نخ ، باید منتظر دریافت ورودی از کاربر باشه (این ورودی ، میتونه انتخاب گزینه ی yes و no توسط موس از کاربر باشه ، فشردن کلیدی در کیبرد باشه و ...) اما کارهای دیگه ای هست که هیچ ربطی به این کارها (یی که بعد از دریافت ورودی از کاربر که باید انجام بشه) ، ندارن . بنابراین دو کار کاملا متفاوت میتونن کاملا جدای از هم انجام بشن . بنابراین دو نخ مجزا ، برای دو کارِ متفاوت ایجاد میکنن و هر کدوم را در نخ ای انجام میدن . مثلا وقتی که از کاربر یک سئوالی برای حذف فایلی میپرسیم (در صورت جواب مثبت ، میخوایم فایل حذف بشه) ، و همون زمان هم نیاز داریم مثلا یک عکس و تصویری را در فایل png در هارد یا در رم ذخیره کنیم . چون این دو کار ، هیچ ربطی به هم نداره ، میتونیم در دو نخ متفاوت اجراشون کنیم . بنابراین هر وقت ، کاری وجود داشت که هیچ ربطی به کار قبلی نداشت ، این کار جدید را در نخ جدید انجام میدن .

اما اگه (در همین قضیه ی حذف فایل) نخ جدیدی اجرا نمیکردیم و فقط در یک نخ اجرا میکردیم ، چی میشد؟
چون کدها در هر نخی که اجرا میشن ، به ترتیب اجرا میشن و کدی اجرا نمیشه مگر اینکه کد قبلی اش اجرا شده باشه ، بنابراین وقتی که کد حذف فایل را نوشتیم و بعدش کد ذخیره ی تصویر در هارد را نوشتیم ، در این صورت ، اول وقتی پیام MessageBox.Show را که دادیم و از کاربر پرسیدیم که میخوای فلان فایل را حذف کنی یا نه ، این متد ، کد را در همون نخ متوقف میکنه (هر وقت کد ، در نخی متوقف بشه ، ادامه ی کدها در همون نخ اجرا نمیشه . واسه ی همینه که تا به MessageBox.Show جوابی ندی ، کدهای خط بعدش که نوشتی ، اجرا نمیشه) تا کاربر بهش جوابی بده . این جواب ، ممکنه مثلا 10 دقیقه طول بکشه . یعنی ، مثلا کاربر ، 10 دقیقه ی بعدش بیاد جواب بده . بنابراین کدِ بعد از MessageBox.Show هم بعد از 10 دقیقه تازه اجرا میشه . یعنی بعد از 10 دقیقه ، تازه اون عکس ذخیره میشه در حالی که این کارمون (ذخیره ی عکس)، هیچ ربطی به جواب کاربر نداشت و میتونستیم همون موقعی که به کاربر پیام میدیم که میخواد فایلش حذف بشه یا نه ، اون عکس را هم ذخیره کنیم و کارهای مربوط به اون عکس را هم برسیم .
حالا ، در این مثال ، تعداد کارهایی که میتونست مجزای از همدیگه انجام بشه ، 2 تا بود و بنابراین یه نخ جدید میخواست (چون یه نخ اصلی که همیشه در حال اجرا هست و میتونیم یه کار را توش انجام بدیم . هر چند باز هم برای دو تا کار ، علاوه بر نخ اصلی ، میتونیم دو تا نخ جدید دیگه هم بسازیم که با نخ اصلی ، 3 نخ در حال اجرا در اون موقع میشه) اما ممکنه در یک موقع ، 10 تا کار جدیدِ مجزای از هم برامون پیش بیاد بنابراین میتونیم 9 نخ جدید در همون موقع اجرا کنیم . بسته به تعداد کارهای مجزامون داره .


اینکه چه پروسه ای ، در حال حاضر داره چند تا نخ را اجرا میکنه را میتونی از پنجره ی Resource Monitor (قسمت یا سربرگ CPU و ستون Threads) ببینی . که معمولا هم در حال تغییر هستند ، مخصوصا اگه برنامه ای فعال و در حال کار کردن باشه (البته باز هم بستگی داره به برنامه نویس اون برنامه که نخی درست کرده باشه یا نه و اون نخ را متوقف کرده باشه یا نه و چه مدت زمانی اجرای اون طول بکشه) (البته اگه برنامه ی سی شارپ را چک میکنی ، احتمالا خود clr ، چند تا نخ دیگه هم برای برنامه های دات نت میسازه و به همین دلیل هست که برنامه های دات نت ، زمان اجرا ، چندین نخ دارن بدون اینکه برنامه نویس ، خودش نخ فرعی ای را اجرا کرده باشه (دقیق نمیدونم . حالا در این باره ی clr ، از کسایی که مطلع هستند سئوال کن)) . اگه هم میخوای ببینی مجموع برنامه های در حال اجرا در کامپیوترت ، چند نخ در حال اجرا دارند ، در Task Manager ، در سربرگ Performance برو و در قسمت پایینی ، نوشته ی Threads را ببین (معمولا وقتی کامپیوتر روشن میشه و بدون هیچ نرم افزار اضافی ای ، بیش از 1000 نخ هست که مدام در حال تغییر هه) .


نخ ها را سیستم عامل کنترل میکنه . سیستم عامل ، از بین نخ های زیادی که پروسه های مختلف برای پردازش به سیستم عامل درخواست دادن ، نخ های مورد نظرش را انتخاب میکنه و برای پردازنده برای پردازش کردن میفرسته . اینکه مثلا زمانی که 23 تا نرم افزار (پروسه) رو فورا بصورت همزمان اجرا کنیم (پس از اونجایی که هر پروسه ، حداقل یک نخ دارن ، پس 23 نخ مجزا ، بصورت همزمان اجرا میشن) (البته ، کلمه ی فورا که گفتم ، شاید عبارت دقیقی نباشه که بعدها مفصل تر توضیح بدم ، متوجه میشی) ، و پردازنده ی ما هم 6 هسته ای با 12 هسته ی منطقی (یا همون 12 نخ که گفتم که اصطلاح نخ برای پردازنده درست نیست) باشه ، پس ، حداکثر 12 نخ (در اینجا ، پروسه) را میتونه همزمان اجرا کنه بنابراین 11 نخ دیگه (در اینجا ، 11 پروسه ی دیگه) باقی میمونه اما اینکه پردازش کدوم نخ ها از اون 12 نخ ، مهمتر از اون 11 نخ هستن که زودتر از اون 11 نخِ دیگه پردازش بشن را سیستم عامل تعیین میکنه (با الویت بندی ای که برنامه نویس برای اون نخ مشخص میکنه . این الویت بندی ، در سربرگ Details از Task Manager ، با کلیک راست روی پروسه ی مورد نظر و انتخاب Set Priority و انتخاب گزینه ی مورد نظر ، علاوه بر اینکه در دسترس کاربران ویندوز هست تا بتونن الویت اجرای نخ اصلی پروسه ها را مشخص کنن ، وجود داره (دقت کن که کاربران ویندوز نمیتونن الویت بندی نخ های فرعی اون پروسه را مشخص کنن) ، در دسترس برنامه نویس ها هم هست تا بتونن الویت بندی نخ های فرعی خودشون را تعیین کنن که این الویت بندی ، باید قبل از اجرای نخ انجام بشه نه بعد از اجرای نخ) .


بقیه ی مبحث نخ ها را میذارم که عملکرد پردازنده ها را توضیح دادم . عملکرد پردازنده ها را هم در پست های بعدی توضیح میدم چون توضیح این پست زیاد شد .

SajjadKhati
22-11-19, 21:45
پردازنده ها ، از هسته های مختلف ممکنه تشکیل شده باشن (خیلی وقته دیگه پردازنده ی تک هسته ای نداریم . امروز ، حداقل از دو هسته تشکیل شدن) که به این هسته ها ، physical core یا هسته ی فیزیکی میگن . اگه نمونه ی مثال میخوای ، مثل پردازنده ی Ryzen 3 1200 که از 4 هسته ی فیزیکی تشکیل شدش . همچنین ، مثل اغلب پردازنده های روز.
هر هسته ی فیزیکی ، میتونه یک نخ را (بصورت همزمان) پردازش کنه.بنابراین پردازنده ای که ۴ هسته ی فیزیکی داره (مثل پردازنده ی Ryzen 3 1200) ، همزمان میتونه فقط ۴ نخ را پردازش کنه.
البته ، این ، به این معنی نیست که اگه یه پروسه ای ، بصورت پیش فرض ، ۴ تا نخ را همزمان اجرا کرده باشه ، این نخ ها در ۴ هسته ی فیزیکی اجرا میشن پس بنابراین پردارشِ این ۴ نخ ، همزمان خواهد بود . در این باره (رابطه ی پروسه و نخ های اجرا شده ی در یک پروسه و همچنین نخ های اجرا شده توسط پروسه های مختلف) ، احتمالا در پست بعدی میگم.



همینطور اینکه بعضی از پردازنده ها ، هسته هاشون ، ممکنه از پردازنده ی مجازی یا همون logical core یا همون هسته ی منطقی تشکیل شده باشه که این هسته ی منطقی ، در شرایط محدودتری ، میتونن دو تا نخ (مثلا دو تا پروسه) را بصورت همزمان در هر هسته ی فیزیکی پردازش کنن .
اگه پردازنده ای ، تکنولوژی هسته ی منطقی (یا به اصطلاح ، تکنولوژی hyper threading) رو داشته باشه ، در این صورت ، هر هسته ی فیزیکی اش از دو هسته ی منطقی تشکیل میشه که هر هسته ی منطقی اش ، اگه شرایطش جور باشه ، (نه اینکه هر وقت دلش خواست) ، پردازش همزمان را انجام میده بنابراین ، هر هسته ی فیزیکی در این تکنولوژی ، اگه شرایطش جور باشه ، میتونن همزمان ۲ نخ را پردازش کنن. مثلا اگه پردازنده ی ۶ هسته ی فیزیکی با ۱۲ هسته ی منطقی باشه مثل Ryzen 5 3600 ، اگه شرایط جور باشه ، میتونه ۱۲ نخ را بصورت همزمان پردازش کنه. (البته این نوع پردازنده ها ، عموما به اشتباه ، ۶ هسته ۱۲ نخ شناخته میشن. حتی خودمم از این اصطلاح استفاده میکنم اما اگه سایت حرفه ای رو رجوع کنی ، هیچ وقت اصطلاح نخ برای پردازنده بکار نمیبره بلکه اصطلاح logical core را بکار میبره که درستش همینه) .

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

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



درباره ی حافظه در پردازنده هم که دو نوع حافظه داریم . یکی کش و یکی هم رجیستری .
حافظه ی رجیستری که مهمترین و پر سرعت ترین حافظه در پردازنده هست ، در هر هسته ی فیزیکی ، حافظه ی رجیستری مجزایی وجود داره و عملیات های مهم پردازنده (مثل خوندن و نوشتن روی متغبیرها یا آدرس حافظه ی رم یا آدرس حافظه ی کش ای که قراره پردازنده از توی اون آدرس ، اطلاعات را بخونه یا توش بنویسه) توی این حافظه ی رجیستری ذخیره میشه .
حافظه ی رجیستری هم انواع مختلف داره که در مقاله ی زیر میتونی باهاش آشنا بشی :

Only the registered members can see the link

حافظه ی بعدی هم کش هست که فکر کنم نسبت به اینکه کش سطح l1 یا l2 یا l3 باشه ، میتونه بصورت اختصاصی در هر هسته ی فیزیکی ، بصورت مجزا استفاده بشه یا بصورت مشترکی برای تمام هسته های فیزیکی (کل پردازنده) استفاده بشه .

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

در واقع ، 3 بار انتقال اطلاعات صورت میگیره . یکبار اطلاعات اساسی ای که لازم هست ، از هارد ، به حافظه ی رم منتقل میشه (چون خود هارد هم page file داره که اگه رم کم اومد ، در امتداد حافظه ی رم شناخته میشه و کار رم را انجام میده و سیستم عامل ، این page file را کنترل میکنه و اطلاعاتی که فعلا لازم نیست یا مقدار رم اگه کم باشه را در page file که در هارد هست ، ذخیره میکنه و هر وقت نیاز به این اطلاعات بود ، به حافظه ی رم منتقل میکنه) ؛ یکبار دیگه ، اطلاعات خیلی مهمتر (در همون لحظه بهش نیاز هست) ، از رم به حافظه ی کش پردازنده منتقل میشه و سر آخر که همه ی این انتقالات بخاطر اینه که اطلاعات زودتر در دسترس حافظه ی رجیستری قرار بگیره ، اطلاعاتی که دقیق در همون لحظه (شاید در همون میلی یا میکروثانیه . این میزان از زمان که گفتم ، ممکنه گاها بزرگتر هم بشه) نیاز هست ، به حافظه ی رجیستری در هر هسته ی فیزیکی منتقل میشه برای ثبت اطلاعاتی که برای پردازش نیاز هست (مثلا دو عدد را که میخواد جمع کنه ، اون دو عدد ، درون حافظه ی رجیستری ثبت میشن . پردازش که شدن ، نتیجه اش باز درون حافظه ی رجیستری ثبت میشه . چون رجیستری ، حافظه ی بسیار کوچیکی هست ، معمولا این نتیجه در اون بصورت دائم ذخیره نمیشه . فقط در این حد ذخیره میشه تا اون رو فورا در حافظه ی کش یا مخصوصا در حافظه ی رم ذخیره کنه . آدرس این حافظه از کش یا آدرس حافظه از رم که قراره این نتیجه در اونجا نوشته بشه هم در رجیستری ذخیره میشه) .

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

درباره ی حافظه ی کش که گفتم ، حالا از کسایی که دقیق تر میدونن ، سئوال کن . ولی اطلاعات کلی اش ، احتمالا همینی هه که گفتم . اینترنت هم وصل نیست که اطلاعات دقیق تر بگیرم.،



بقیه هم در پست بعدی .

ravegoat
23-11-19, 18:31
درود
...

سلام آرشان جان،

بنده سعی می کنم یه دید کلی برای هر سوالت فراهم کنم که در تکمیل فرمایش های سجاد جان باشه. برای اینکه سو برداشتی نباشه بنده هم CPU Thread رو تحت عنوان هسته ی منطقی و Software Thread رو تحت عنوان نخ به کار می برم.


س1) از کجا بفهمیم که یک نرم افزار از یک ترد استفاده می کنه یا دو ترد به صور همزمان. توی نرم افزارهای بزرگ این مورد نوشته می شه مثل Cinem4D ولی برای خیلی ها نوشته نمی شه
برای این کار ابزار های Profiling وجود داره که توسط شرکت های مختلف عرضه میشه. یکی از بهترین گزینه های موجود Intel® VTune™ Amplifier هستش که اطلاعات کاملی در مورد تعامل برنامه با پردازنده ارایه میده.



س2) فرض کنید یک پردازنده 6 هسته ای دارم با فرکانس 3 گیگاهرتز و 12 ترد. حالا می خوام چند تا برنامه به طور همزمان باز کنم و این برنامه ها به فرض رندر می کنن. حالا این نرم افزارها و عمل رندر به کدوم یک از روش هایی که میگم انجام می شه. این حالت ها برای نرم افزارهای 1 تردی یا دو تردی فرق دارن


الف) نرم افزار اول باز شده و در حال رندر کردنه و 2 هسته از سی پی یو رو اشغال می کنه و از هر هسته فقط 2 گیگاهرتز اشغال می کنه. بعد نرم افزار دوم رو باز می کنم و اون هم به دو هسته نیاز داره و چون از هر یک از دو هسته اول یک گیگاهرتز باقی مونده اول اون دو تا یک گیگاهرتز از هتسه اول رو اشغال می کنه و باقی پردازش میره روی هسته های 3 و 4 و همینطور که نرم افزارهای دیگه رو باز می کنم هسته های بعدی درگیر میشن تا 6 هسته پر بشه و 100 درصد پردازنده اشغال بشه


ب) نرم افزار اول باز شده و در حال رندر کردنه و پردازش روی 6 هسته پخش می شه و همینطور که نرم افزارهای بعدی که باز می کنم پردازش اونا هم روی این 6 هسته پخش می شه و به طور کلی این 6 هسته کم کم پر می شه تا 100 درصد پردازنده اشغال بشه.
شاید بهتر باشه بگیم هیچ کدوم از این حالت ها اتفاق نمی افته. قضیه پیچیده تر از این حرف ها هست. اینکه برنامه چطوری اجرا میشه به سه تا عامل بستگی داره: ساختار برنامه که برنامه نویش اون رو تعیین می کنه، سیستم عامل و پردازنده.


س3) ما برنامه نویسی موازی داریم. این برنامه نویسی موازی در نرم افزار باعث می شه که عمل رندر با دو سی پی یو همزمان انجام بشه یا اگه سی پی یو 8 هسته ای داشته باشیم روی هر 8 هسته انجام می شه که شاید دیگه نشه بگیم موازی چون به جای دو هسته داره روی 8 هسته انجام می شه.
موازی سازی سطوح مختلفی داره. در مثالی که زدی عمل رندر روی دو پردازنده به شکل همزمان و هم عمل رندر روی هشت هسته به شکل همزمان، دو مدل متفاوت از موازی سازی هستند.


س4) الان بیشتر پردازنده های اینتل مثلا 6 هسته و 6 ترد دارن ولی AMD 6 هسته و 12 ترد داره. و من در جایی خوندم که هر کدوم مزایا و معایبی دارن. بعضی جاها این دو ترد داشتن مهمه و بعضی جا ها تک ترد بودن. می خوم کمی در این رابطه توضیح بدی. اصلا اگه دو ترد داشتن خیلی خوبه چرا اینتل گیر داده به تک ترد روی بیشتر پردازنده هاش.
این بحث که پردازنده n تا هسته ی فیزیکی داره ولی 2n تا هسته ی منطقی فراهم می کنه تحت عنوان Simultaneous MultiThreading یا SMT شناخته میشه. این ویژگی در پردازنده ی های اینتل با نام Hyper-threading معرفی شده. وقتی یه نخ برای ادامه ی کار منتظر یه سینگال مشخصه (مثلا منتظر یه پاسخ از سمت شبکه است)، هسته عملا نمی تونه کاری با اون نخ بکنه و بیکار می مونه تا سیگنال دریافت بشه. ویژگی SMT سبب میشه هنگام بیکاری هسته، یه نخ دیگه روی اون به اجرا در بیاد و عملا انگار یه هسته ی فیزیگی، می تونه دو تا کار انجام بده.
سه نکته درباره ی ویژگی SMT وجود داره:

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


این که اینتل و AMD پردازنده های نسل جدید خودشون رو با SMT یا بدون اون عرضه کنن بستگی به سیاست های تجاری اون ها داره. اینتل یه جورایی پیشگام استفاده از SMT در پردازنده های خودش بود و بعد از اون AMD هم در معماری بولدوزر به این ویژگی گرایش پیدا کرد ولی پیاده سازی SMT در این پردازنده های این دو شرکت با هم متفاوته. تا اون جا بنده یادمه در اینتل حافظه ی نهان (Cache) سطح یک و دو در دو هسته ی منطقی با هم مشترکه که سبب بروز مشکلات امنیتی زیادی برای اینتل در این اواخر شد. اما در معماری بولدوزر AMD هر هسته ی منظقی، یه حافظه ی نهان سطح یک مخصوص به خودش رو داشت.


س5) پردازش در سی پی یو ها به صورت صف عمل می کنه یا پشته.
سیستم عامل فرمان ها رو در یک صف پردازشی قرار میده ولی در عمل صف نیست. اون موقع که من تو کوچه گل کوچیک بازی می کردم، اگر نخی نرخ پاسخ گویی پایین تری پیدا می کرد، پردازنده به شکل پویا سیکل محاسباتی بیش تر رو بهش اختصاص میداد. دیگه چه برسه به الان...


س6) خیلی از ویژگی هایی که در پردازنده های اینتل وجود داره در پردازنده های AMD نیست یعنی کاربردی ندارن. (منظورم از ویژگی ها مثلا SSE4.2 هست و امثال این ها )
به نظرم باید بگی کدوم ویژگی ها چون SSE4.2 تا اون جا که بنده می دونم روی خیلی از پردازنده های AMD پشتیبانی میشه (گرچه اینتل این ویژگی رو معرفی کرده)!


س7) اگه معماری پردازنده ها رو در نظر بگیریم چه فرقی بین نسل 3000 رایزن و نسل 9 اینتل وجود داره. عمل پردازش اینا به یک شکله یا هر کدوم به شیوه خودشون پردازش می کنن. چون در خیلی جاها من دیدم که اینتل با تعداد هسته کمتر قدرتی تقریبا برابر AMD داره( به طور کلی میگم). کلا یک مقایسه ای وجود داره که بفهمیم معماری که اینتل در پردازنده هاش داره بهتره یا AMD در آخرین نسل.
نکته ای که وجود داره پردازنده های x86-64 باید از یه مجموعه دستورالعمل (ISA) مسخص پشتیبانی کنن وگرنه برنامه نویس ها باید برای هر پردازنده یه کد جا بنویسن. اینتل و AMD در این دسته از پردازنده ها، ISA یکسانی دارن ولی نحوه ی پیاده سازی ISA که همون ریزمعماری هستش متفاوته. در هر نسل شرکت ها فناوری ساخت و ریزمعماری رو توسعه میدن ولی هر ریزمعماری می تونه برای دسته ای از عملیات ها بهینه باشه و برای دسته ی دیگه نباشه. در نتیجه حتی اگر دو پردازنده مختلف فرکانس یکسانی داشته باشن، سرعت اجرای یه برنامه ی مشخص برای این دو می تونه متفاوت باشه! خلاصه ی کلام: خیر، در حال حاضر چنین مقایسه ای وجود نداره.

آرمین :11():

SajjadKhati
30-11-19, 14:22
سلامی مجدد .
خیلی ممنون از پست خوب مهندس آرمین . اطلاعات ارزشمندی رو گفتن .




میخواستم پست شماره ی 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" . همونطور که میدونی ، این دستور ، برابر با دستور



int i = 0;
i = i + 1;


هست . بنابراین ، دستور اول ، خوندن مقدار 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 مون ، الویت مشخص کنیم ، مینویسیم :



Thread thread = new Thread(this.NewThreadMethod);
thread.Priority = ThreadPriority.Highest;
thread.Start();



که الویتِ Highest ، بالاترین الویت را در نظرمیگیره . این به این معناست که اگه همزمان مثلا 100 نخ (از هر پروسه ای) ، به سیستم عامل ، درخواست اجرا بدن ، نخ 2 مون و همچنین همه ی نخ هایی که این الویت را دارند ، سیستم عامل این نخ ها را زودتر از بقیه ی نخ ها ، برای اجرا ، به پردازنده ارسال میکنه .
اما اگه در همون لحظه ، فقط این نخ (نخ 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 گیگاهرتز را روی این نخ میذارم برای پردازش و در همین زمان ، بقیه ی فرکانس اش را برای نخ دیگه ای برای پردازش میذارم . قضیه ی پردازش و اجرای نخ را که در همین پست توضیح دادم (اینو گفتم چون در سئوالاتت بود)

در پست بعدی ، به کدهای دیگه ی چند نخی میپردازیم .

SajjadKhati
01-12-19, 13:58
kernel time :

اگه Task Manager را باز کنی و در سربرگ Performane و در قسمت Cpu را انتخاب کنی و در قسمتی که نمودار کارکرد پردازنده را داره رسم میکنه ، کلیک راست کنی ، یه گزینه ای بنام show kernel time وجود داره که اگه تیک بزنیش ، در قسمت دیاگرام و نمودار پردازنده (جایی که کارکرد و مقدار اشغال پردازنده را رسم میکنه) ، یه نمودارِ آبیِ پر رنگ تر ، در قسمت پایین ترِ اون نمودار رسم میکنه که بهش kernel time میگن . این نمودار هم مثل کارکرد cpu ، مدام در تغییر و بالا و پایین تر رفتن هست .

kernel time ، مقدار پردازشی که اون هسته ی منطقی از کدهای سیستم عامل و درایورها انجام میده را رسم میکنه .
مثلا در برنامه ی سی شارپ ای که نوشتی ، خود سی شارپ که با پردازنده در تماس نیست و کدهاش را تحویل پردازنده نمیده . (اصلا وقتی برنامه ای که در سی شارپ نوشتی را اجرا کنی ، دیگه بعد از اون سی شارپ معنا نداره . اونجا دیگه زبان clr هست که کدهای سی شارپ ات را باید تفسیر و کارهای مربوط بهش را انجام بده) .
اگه اشتباه نکنم ، clr با سیستم عامل در تماس هست و سیستم عامل هست که مستقیما با اون منبع سخت افزار مثل پردازنده در تماس هست .

بنابراین علاوه بر کدهای خودت که در سی شارپ نوشتی ، کدهای اضافه تری مثل کدهای clr و همچنین کدهای سیستم عامل هم باید اجرا بشه . اون نمودار kernel time ، این کدها را نشون میده (احتمالا باید کدهای clr هم جزء اش باشه . دقیق نمیدونم). یعنی وقتی برنامه ای که اجرا میشه ، کدهای بخش سیستم عامل و درایورهایی که برای اجرای اون کد از برنامه اجرا میشه را نشون میده .
بنابراین ، هر چی که نمودار kernel time ، کمتر باشه ، یعنی کدهای مربوط به سیستم عامل (و بقیه مثل درایور و اگه برنامه ی دات نت باشه ، احتمالا کدهای clr) کمتری اجرا شد و در عوض کدهای اون برنامه ، بیشتر اجرا شد .
در واقع ، یعنی کدهای برنامه ای از همه بهینه تر هه که کدهای بخش kernel time کمتر (نمودارِ آبیِ پر رنگ ، کمتر) و بخش کدهای مربوط به اون کد از برنامه ، یعنی بخش نمودار آبیِ کم رنگ ، بیشتر باشه .

اگه کدی که در پست قبلی دادم را چک کنی ، میبینی کدهای kernel time اش بصورت میانگین زیر 10 درصد هست (که البته متغییر هه . از 5 تا 15 درصد) . یعنی بقیه اش داره کدهای برنامه ی ما اجرا میشه . که این ، یک نمونه ی خیلی خوب از برنامه هست و یعنی کدها بهینه هست (البته ، این که فقط حلقه ی خالی هه . کلا میخوام بگم که کدی که این جوری باشه ، بهینه هست) .
البته دقت کن که kernel time به معنای توقف و سربارهای اون هسته ی منطقی نیست . یعنی نمیگه که اون هسته ی منطقی ، چقدر از زمانش را هدر داد . بنابراین ، منظورم از بخش بالا که گفتم "یعنی بقیه اش داره کدهای برنامه ی ما اجرا میشه" ، این نیست که پردازنده سربار نداره (یا سربارش کم یا زیاد هست) . بلکه به این معناست که در اون لحظه ای که اون هسته ی منطقی داره کد برنامه مون را اجرا میکنه ، بصورت میانگین ، فقط 10 درصد داره کدهای سیستم عامل (و کلا کدهایی که بصورت مستقیم به برنامه ی ما ربط نداره و ما ننوشتیم) را اجرا میکنه و بقیه اش کدهای ما را اجرا میکنه .

اما معمولا اگه یه هسته ی منطقی ای سربار زیادی داشته باشه (که به معنای هدر رفت و از دست دادن زمان برای اون هسته ی منطقی هست) ، نمودار کارکرد در Task Manager ، معمولا هی کم و زیاد میشه . و کمتر به حداکثرِ کارکردش ، یعنی به 100 در صد نزدیک میشه . چون مدام در حال سوئیچ بین نخ هاست . چون کدهای یک نخ (متد یا متغییر) ، هنوز در حافظه ی رجیسترش بارگذاری نشد که تا رسیدنش ، میتونه به نخ دیگه سوئیچ و اجرا کنه . البته اینی که گفتم (همین پاراگراف . درباره ی تغییرات بیشتر در نودار Task Manager ، احتمال سربار بیشتر) ، سند واسش ندارم . تحلیل خودم هست . فکر نکنم اشتباه باشه . دقیق نمیدونم .




یه مثال دیگه :
اول ، یه کنترل listbox بنام listBox2 را در فرم ات اضافه کن و همچنین یه کنترل TransparentControl که بهت دادم را بنام TransparentControl4 اضافه و رویداد کلیک اش را به متد TransparentControl4_Click در کد زیر متصل کن و کد :




private void TransparentControl4_Click(object sender, EventArgs e)
{
this.listBox2.Items.Clear();
Application.DoEvents();


Thread thread = new Thread(new ThreadStart(this.NewThreadMethod));
thread.Start();


for (int i = 0; i < int.MaxValue; i++)
{
this.listBox2.Items.Add("Main Thread . i = " + i.ToString());
Application.DoEvents();
}

}


private void NewThreadMethod()
{
for (int counter = 0; counter < int.MaxValue; counter++)
{
this.Invoke(new AddToListBoxInMainThreadDelegate(this.AddToListBox InMainThread), counter);
Application.DoEvents();
}
}


private delegate void AddToListBoxInMainThreadDelegate(int parameters);


private void AddToListBoxInMainThread(int parameters)
{
this.listBox2.Items.Add("New Thread . counter = " + parameters.ToString());
}



در این کد ، سوئیچ نخ ها را بهتر و قشنگ تر درک میکنی .
در کنترل listBox2 ، هر جا اول متن ، "Main Thread" نوشته شد ، یعنی اون حلقه ای که در نخ اصلی بود اجرا شد و هر جا اول متن ، "New Thread ......." نوشته شد ، یعنی حلقه ای که در نخ جدید بود (نخ ای که در متغییر thread ذخیره شده) ، اجرا شد .
همونطور که میبینی ، درهم و برهم این پیام ها را میده . یه بار مثلا 50 بار پیام "Main Thread" (اول پیام) را میده ، بعدش مثلا 10 بار پیام "New Thread ......." (اول پیام) را میده . یه وقت 2 بار اینو میده و .... .
همونطور هم که میدونی هر بار پیام ها که عوض میشن ، یعنی در اون لحظه ، به اون نخ سوئیچ کرده بود .








حالا ، اگه کد بالا را اجرا کنی ، نسبت به کد در پست قبلی ، با اونکه هر دو کد ، 2 نخ دارند و در دو نخ اجرا میشن ، چند تغییر را در Task Manager میبینی :



1) اول اینکه نسبت به کد قبلی ، نمودارِ کارکردِ هسته (های) منطقی (در Task Manager) ، خیلی بیشتر (از کد قبلی) بالا و پایین میره و خیلی نوسان بیشتری نسبت به کد قبلی داره و حتی به کارکرد حداکثر خودش که 100 درصد باشه نمیرسه (بسته به پردازنده داره ها ولی برای من ، کارکرد اون هسته ی منطقی ، تا 70 درصدش میره) .

قبل از توضیح دلیل این مسئله ، اینو بگم که در پست قبلی من گفته بودم که وقتی کدی اجرا میشه ، پردازنده با تمام توان و فرکانس اش (یعنی با فرکانس 100 درصد اش) اون کد را اجرا میکنه اما اگه این طور بود ، پس توی Task Manager ، کارکرد اون هسته ی منطقی را همیشه باید تا 100 درصد نشون میداد اما ما خیلی اوقات میبینیم که کارکرد پردازنده مثلا در اوقات بیکاری 10 درصد هست یا مثلا وقتی همین کد (در این پست) را اجرا میکنیم ، مثلا در پردازنده ی من ، 70 درصد از یه هسته ی منطقی اشغال هست (و 30 درصدِ بقیه اش خالی هست) یا در بهینه ترین کد هم خیلی کم میبینیم که عملکرد اون هسته ی منطقی بیشتر از 98 درصد بره . پس دلیل اینها چیه؟

دلیل اینها ، سرعت آپدیت بسیار پایینِ Task Manager هست . در واقع اون هسته ی منطقی وقتی بخشی از کد یک نخ را اجرا میکنه ، در چند میکروثانیه و دیگه خیلی زیاد در کمتر از چند میلی ثانیه انجام میده و بعد سوئیچ میکنه روی نخ دیگه اما بیشتر سرعت آپدیت Task Manager حدود 500 میلی ثانیه (0.5 ثانیه) هست که بین این اختلاف زمانیِ 0.5 ثانیه ، پردازنده بارها و بارها سوئیچ انجام داده .
در واقع ، Task Manager ، میانگین عملیات را حساب میکنه نه لحظه ی واقعی و دقیق عملکرد اون هسته ی منطقی را .


حالا در جواب سئوال شماره ی 1 که دلیل نوسان بیشتر این کد نسبت به کد قبلی بود ، در ختم کلام ، سوئیچِ بیشترِ این کد نسبت به کد قبلی هست در هسته ی منطقی هست . اما چرا سوئیچ در این کد بیشتر هه؟

مهمترین دلیلش اینه که کنترل ها (مثل button و list box و اینها) ، اگه بخوایم عملیاتی در اونها انجام بدیم مثلا در کنترلی ، رسم ای (نوشتن و تغییر متن) انجام بدیم و کلا با کنترلی کار کنیم ، مجبوریم فقط درونِ هر نخ ای که اون کنترل را ایجاد کردیم ، این عملیات (مثلا تغییر متن یا همون رسم) را انجام بدیم (چون ساختار کنترل ها از اول این جوری طراحی کردن که در یک نخ فراخونی بشه و در یک نخ کار کنه) . و از اونجایی هم که همه ی کنترل ها را به کنترل form1 مون اضافه میکنیم ، بنابراین مجبوریم وقتی با بقیه ی کنترل ها کار میکنیم ، درون همون نخ ای کار کنیم که کنترل form1 مون را ایجاد کردیم و چون کنترل form1 را درون متد Main (در کلاس Program) ایجاد کردیم و این متد Main هم همیشه درون نخ اصلی اجرا میشه (clr ، متد Main را فراخونی و در نخ اصلی اجرا میکنه) ، بنابراین مجبوریم با هر کنترلی که کار میخوایم کنیم ، کارِ مربوط به اون کنترل را درون نخ اصلی انجام بدیم .

بنابراین ، اگه درون هر نخ ای هم که باشیم ، مجبوریم برای کار با اون کنترل (گفتم دیگه . مثل رسم ، فراخونی اغلب متدهای مربوط به اون کنترل و ...) ، اون را در نخ اصلی فراخونی کنیم .
برای این کار (اینکه از درون یک نخ ، یه کاری را درون نخ ای که اون کنترل در اون ساخته شد که همون نخ اصلی هست ، انجام بدیم) ، باید از متد Control.Invoke استفاده کنیم .
بنابراین کدی که در نخ جدید (یعنی متد NewThreadMethod) نوشتیم ، در حلقه ی این متد ، نمیتونیم بگیم رسم (نوشتن متن) ای را به کنترل listBox2 اضافه کنه چون این متد ، در نخ اصلی قرار نداره (توضیح دادم دیگه کنترل ها در نخ اصلی کار میکنن) و باید با متد this.Invoke ، کد مربوطه اش را در متدی بنویسیم که اون متد در نخ اصلی فراخونی بشه . بنابراین ، متد AddToListBoxInMainThread (که متد this.Invoke ، متد AddToListBoxInMainThread را فراخونی میکنه) ، در نخ اصلی اجرا میشه .

به عبارتی دیگه ، در حلقه ی نخ جدید (در حلقه ی متد NewThreadMethod) ، هر بار کد را که اجرا میکنه ، مجبوره سوئیچ کنه به نخ اصلی و کد را در نخ اصلی انجام بده (این سوئیچ ، ممکنه در همون هسته ی منطقی ، یا هسته ی منطقیِ دیگه رخ بده) که همونطور که در پست قبلی توضیح داده بودم ، مثلا 5 میکرو ثانیه ، کد را اجرا میکنه و 50 میلی ثانیه را صرف سوئیچ نخ میکنه (چون سوئیچ را به ازای اجرای هر بار در حلقه ی متد NewThreadMethod ، حتمی و اجباری کردیم) .

نکته ی مهم دیگه اینه که چون هیچ نخی را همزمان ، 2 تا هسته ی منطقی اجرا نمیکنه و این در حالی هست که قبلا در یک هسته ی منطقیِ دیگه ، مشغول اجرای نخ اصلی در برنامه مون هست ، پس وقتی کارِ یه هسته ی منطقی دیگه در نخ جدیدمون (یا NewThreadMethod) تمام شد ، یعنی وقتی به کد this.Invoke در حلقه ی این نخ جدید (در متد NewThreadMethod) رسید و بنابراین میتونه به نخ اصلی برنامه مون سوئیچ کنه ، اما این سوئیچ ای را هم که سربار هم داره ، انجام نمیده چون در همین لحظه (ممکنه) هسته ی منطقی دیگه در حال اجرا کردن نخ اصلی مون باشه . و همزمان 2 نخ ، در دو هسته ی منطقی اجرا نمیشه چون ترتیب اجرای کدهای اون نخ ، بهم میخوره .

یعنی عملیات سربار ، خودش اتلاف وقت و انرژی از اون هسته ی منطقی میکنه اما این مشکل که باز هم اون هسته ی منطقی ای که نخ جدیدمون را پردازش کرد ، نمیتونه (ممکنه نتونه) به نخ اصلی مون سوئیچ کنه (چون ممکنه هسته ی منطقی دیگه ای در حال اجرای کد نخ اصلی برنامه مون باشه) که غوز بالای غوز میشه :1. (23):

اینکه کارکرد اون هسته ی منطقی چرا تا آخر نمیره را هم که گفتم .




2) سئوال دوم ای که پیش میاد اینه که موقع اجرای این کد ، اگه در Task Manager ، در سربرگ Details ، عملکرد پروسه ی مون را ببینی ، میبینی که در حد یه هسته ی منطقی (شاید یه کم بیشتر) داره پردازنده را اشغال میکنه . یعنی مثلا در پردازنده ی i5 4460 (چهار هسته ی فیزیکی و 4 هسته ی منطقی) ، در حد یه هسته ی منطقی (و یه کم بیشتر) یعنی 25 درصد (در واقع بین 20 تا 30 درصد) اون هسته ی منطقی را اشغال میکنه که مدام در حال تغییر هست .
در صورتی که ما دو نخ ساخته بودم و هر نخ هم (ممکن بود) در هر هسته ی منطقی مجزا اجرا بشه پس بنابراین مثل کد قبلی ، این کد هم به طبع ، مثلا در پردازنده ای که 2 هسته ی فیزیکی داره ، پروسه مون باید 100 درصد از اون پردازنده را مشغول میکرد (یا در پردازنده ی ای مثل 4460 ، باید 50 درصد از اون پردازنده را مشغول میکرد ولی چرا حداکثر 30 درصدش را مشغول میکنه؟) . چرا مثلا در پردازنده ی دو هسته ای ، دو هسته ی منطقی را مشغول نمیکنه؟ دلیل این چیه؟


چون همونطور که توضیح دادم ، با هر اجرای حلقه در نخ جدید (و اجرای متد this.Invoke در این نخ) ، به نخ اصلی باید سوئیچ کنه . هر نخ (مثل نخ اصلی) را هم در یک لحظه ، فقط درون یک هسته ی منطقی میتونه اجرا بشه .
اضافه کردنِ (عدد 1) به مقدار متغییر counter در نخ جدید که نسبت به اجرای کد this.Invoke در این نخ و بنابراین سوئیچ به نخ اصلی که زمان بسیار ناچیزی حساب میشه بنابراین همونطور که گفته بودم ، بخش عمده را داره بخاطر سربار سوئیچ به نخ اصلی (با اجرای کد this.Invoke) از دست میده .
بنابراین اغلبِ کدها ، بیشتر در نخ اصلی انجام میشه (در سئوال بعدی یعنی سئوال 3 ، بیشتر به این قضیه میپردازیم) و همونطور که گفتم ، هر نخ (مثل این نخ اصلی) ، در یک لحظه ، فقط در یک هسته ی منطقی اجرا میشه . بنابراین در این کد ، بیشتر اوقات (منظورم کل اوقاتش نیست)، فقط یک هسته ی منطقی در حال اجرای کد پروسه ی ماست .

نوسان ای که داره (بین 20 تا 30 درصد در مثلا پردازنده ی 4460 ، مدام در حال نوسان هست) هم بخاطر این سوئیچ های مدام بین این دو نخ هست که در بالا توضیح داده بودم .




3) وقتی این کد اجرا میشه (در Task Manager) ، نمودار kernel time ، بسیار بسیار بالا میره و در حد 60 درصد میره در صورتی که کل کارکرد اون هسته ی منطقی 70 درصد هم نیست (یعنی کدهای clr و مخصوصا سیستم عامل ، 60 درصد از زمان را صرف اجرا میکنه و کدهای (خالص که خودمون نوشتیم) برنامه ی ما ، فقط 10 درصد از زمان را صرف اجرا در اون هسته ی منطقی میکنه) . چرا؟


بخش عمده ی این قضیه برمیگرده به کد رسم (نوشتن متن) در کنترل . در واقع رسم در یک کنترل ، عملیات سنگینی هست که چون کنترل ها ، در واقع (در پشت پرده) با کمک توابع api های سیستم عامل هستند که ساخته (و رسم) میشه بنابراین علاوه بر اجرای کدهای سیستم عامل که باعث میشه kernel time بالا بره ، جزء توابع سنگین به حساب میان . یعنی منابع رم و بقیه ی منابعِ زیادی نسبت به کد ساده ی پست قبلی برای اجرا میطلبن.
بنابراین کد رسم ، یعنی کد this.listBox2.Items.Add که در متد AddToListBoxInMainThread (که این متد در نخ اصلی اجرا میشه) ، اجراش سنگین هست (و به طبع ، انتظار در هسته ی منطقی را بالاتر میبره) و همینطور کد Application.DoEvents که در هر بار اجرای حلقه ، اجرا میشه تا سیستم عامل را وادار که تا فورا و در همون لحظه ، همه ی پیام های (یا همون رویدادهای) مربوط به اون پروسه را برای اجرا ، به پردازنده ارسال کنه .




حالا این کد را جوری میخوایم بنویسیم که زمان اجرای کد در هم نخ اصلی و هم نخ جدیدش را بگیریم . منتها این بار ، تعداد اجراش را خیلی کمتر کنیم . مثلا تعداد اجرای حلقه ها در هر دو نخ را 20000 بار میکنم (زمان ، بر حسب میلی ثانیه هست) :




private void TransparentControl4_Click(object sender, EventArgs e)
{
this.listBox2.Items.Clear();


Thread thread = new Thread(new ThreadStart(this.NewThreadMethod));
thread.Start();


Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();


for (int i = 0; i < 20000; i++)
{
this.listBox2.Items.Add("Main Thread . i = " + i.ToString());
Application.DoEvents();
}


long elapsedTime = stopwatch.ElapsedMilliseconds;
MessageBox.Show(elapsedTime.ToString() + " mili secand", "Main Thread Loop Finished");
}


private void NewThreadMethod()
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();


for (int counter = 0; counter < 20000; counter++)
{
this.Invoke(new AddToListBoxInMainThreadDelegate(this.AddToListBox InMainThread), counter);
Application.DoEvents();
}


long elapsedTime = stopwatch.ElapsedMilliseconds;
MessageBox.Show(elapsedTime.ToString() + " mili secand", "NewThreadMethod Loop Finished");
}




private delegate void AddToListBoxInMainThreadDelegate(int parameters);




private void AddToListBoxInMainThread(int parameters)
{
this.listBox2.Items.Add("New Thread ....... counter = " + parameters.ToString());
}



برای حساب کردن زمان ، بین این دو عدد پیامی که میده ، اونی را حساب کن که عددش بالا تر هه (زمان بر حسب میلی ثانیه هست) .
در پردازنده ی 4460 ، این کد بین 9.8 تا 10.8 ثانیه طول میکشه (هر بار ، متغییر هست ولی کلا بین این بازه هست) .
این کد ، توی هر پردازنده ی دیگه ای هم اجرا بشه (حتی پردازنده های 15 سال پیش . حتی پردازنده ای که یک هسته ی فیزیکی فقط داشته باشه) فکر نکنم اجراش بیش از 25 ثانیه طول بکشه .
همونطور که توی هر پردازنده ی جدیدی و مدرن ای با هر تعداد هسته ای اجرا بشه (مثل پردازنده ی i9-9900K که کلاک تک هسته ایش 5ghz هست یا پردازنده ی threadripper 3970x) فکر نکنم زودتر از 5 ثانیه اجرا بشه .
قیاس ها را نسبت به پردازنده ی 4460 انجام میدم .


به هر حال ، مهم اینه همیشه ، اجرای کدهای در چند نخ و در چند هسته بصورت همزمان ، نتیجه ی خوبی نداره . مثل همین کد .
اگه این کد را در یک نخ انجام بدی ، میبینی که زودتر از حالتی که در 2 نخ انجام دادی ، انجام میشه . دلیلش هم که گفتم (بخاطر سربار بسیار زیادی که پردازنده را مجبور میکنیم و همینطور دلایل دیگه که کامل در همین پست ، در بالا توضیح دادم) .


بنابراین این کد را در یک نخ اجرا میکنیم تا نتیجه را نسبت به زمانی که در دو نخ اجرا کرده بودیم ، ببینیم . دقت کن کد قبلی ، در دو نخ که هر کدوم از نخ ها ، حلقه ای داشتن که 20000 بار تکرار میشد . بنابراین وقتی که کد دو نخ را بخوایم یکی کنیم ، یک حلقه ای باید داشته باشیم که دو برابر تعداد حلقه هاش باشه . یعنی تعداد حلقه هاش 40000 بار باشه :




private void TransparentControl4_Click(object sender, EventArgs e)
{
this.listBox2.Items.Clear();


Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();


for (int i = 0; i < 40000; i++)
{
this.listBox2.Items.Add("Main Thread . i = " + i.ToString());
Application.DoEvents();
}


long elapsedTime = stopwatch.ElapsedMilliseconds;
MessageBox.Show(elapsedTime.ToString() + " mili secand", "Main Thread Loop Finished");
}



این کد (که تک نخی هست) ، در پردازنده ی 4460 ، بین بازه ی 8.3 تا 9 ثانیه ، اجرا میشه .
در صورتی که کد قبلی که 2 نخی بود ، بین 9.8 تا 10.8 ثانیه در این پردازنده اجرا میشد .
یعنی حداقل 0.8 ثانیه تا حداکثر 2.5 ثانیه (میانگین ، 1.8 ثانیه) زودتر از کد قبلی (که 2 نخی بود) اجرا میشه .
مشخص هست هر چی تعداد حلقه زیادتر بشه ، این اختلاف زمانی بیشتر خودش را نشون میده .


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

بنابراین این کاملا به طرز کدنویسی برنامه نویس و مخصوصا اینکه چقدر با کارکرد پردازنده ها آشناتر باشه و بدونه کدهاش در چه وضعیتی در پردازنده ها قرار میگیرن و مخصوصا اینکه بتونه الگوریتم ای طراحی کنه که بتونه کدهاش را جوری تقسیم کنه که با هم تداخل نداشته باشن و دو نخ ای که ساخت ، هر دو نخ ، در یک زمان ، به اطلاعات داده ای (مثلا متغییر) مشترکی نخوان (با هم) دسترسی داشته باشن ، داره .
به این دو کد نگاه نکن . اینها بسیار ساده هستند . هماهنگی بین نخ ها که باعث بهینه تر شدن کدها میشه ، بسیار بسیار کار پیچیده ای هست (منظورم اینه که جوری الگوریتم طراحی بشن که بیشترین هماهنگی بین نخ ها باشه و یه هسته ی منطقی ، منتظر هسته ی منطقیِ دیگری نشه) و شرکت های بزرگ دنیا هم براشون بهینه کردن کدها ، آسون نیست .
واسه ی همینه که هر بار در آپدیت برنامه (های بزرگ) میبینیم که مینویسن کدهامون را بهینه تر کردیم (یک عاملش) .
که البته در این باره (هماهنگی بین نخ ها) ، در پست بعدی میپردازیم .

SajjadKhati
04-12-19, 21:17
هماهنگی بین نخ ها :

اما استفاده از نخ ها ، یا در واقع استفاده از چند هسته بصورت همزمان ، کار ساده ای نیست . یا در واقع میشه گفت جزء پیچیده ترین کارهاست .
این پیچیدگی بخاطر اینه که بخشی از کدی که نوشتیم و در اون بخش از کد، اعضایی را مشترکا در چند نخ بکار بردیم (مثل متغییرهای سراسری یا گاها متغییرهایی که به نخ ها 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 ، بصورت زیر هست :



lock (instance)
{
// code block
}


که در قسمت instance ، شی ای از نوع reference type (مثل شیِ کلاس ها) بهش میدیم . البته این شی ، نباید شی this و همچنین شی ای از string ها باشه (یه نکته ی دیگه این شی داشت که فراموش کردم) . البته من متوجه نشدم این شی برای چی هست .
همونطور که میبینی ، 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 استفاده میکنن .

بنابراین ، چون همگام سازی ، سربار داره و کلا کاهش کارایی داره ، تا جایی که ممکنه ، سعی میشه در چند نخی ها ، از اعضای مشترک استفاده نشه . در وهله ی اول ، فقط از متغییرهای محلی استفاده بشه . اگه قرار به استفاده از متغییرهای سراسری شد ، سعی بشه از متغییرهای جداگانه برای هر نخ استفاده بشه (هر نخ ، متغییرِ جداگانه ی مربوط به خودش را داشته باشه) و در نهایت اگه از اعضای مشترک استفاده شد ، در صورت نیاز ، باید همگام سازی بشه که همگام سازی هم باعث کاهش کارایی میشه اما سعی میشه کمترین کاهش کارایی را داشته باشه (با استفاده از الگوریتم و کدنویسی و کلاس (و روش) مناسب برای همگام سازی) .


مقالات دیگه تمام شد ( البته احتمالا :Love-ssa~! (1): ) . حالا بریم سر وقت جواب پست 1 ات :Love-ssa~! (1): .
موفق باشی .

SajjadKhati
04-12-19, 22:41
درود

س1) از کجا بفهمیم که یک نرم افزار از یک ترد استفاده می کنه یا دو ترد به صور همزمان. توی نرم افزارهای بزرگ این مورد نوشته می شه مثل Cinem4D ولی برای خیلی ها نوشته نمی شه



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

همونطور که گفتم ، برنامه ی Resource Monitor را باز کن (بصورت مجزا یا از در قسمت پایینیِ برنامه ی Task Manager قابل دسترس هه) و در سربرگ اول یا دومِ این برنامه (سربرگ cpu) ، در ستون Threads مینویسه که در حال حاضر ، کدوم پروسه و کدوم برنامه ، چند نخ در حال اجرا داره . (اگه این ستون مشخص نیست ، برنامه را maximize کن) .

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

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






س2) فرض کنید یک پردازنده 6 هسته ای دارم با فرکانس 3 گیگاهرتز و 12 ترد.



اولا ترد نه .
ترد یا نخ ، یه چیز نرم افزاری هست . چیزی که سیستم عامل باهاش سر و کار داره . پردازنده ، نخ را نمیشناسه . نخ ، چیزی هه که در سی شارپ ، از کلاس Thread ، شی میسازی .
بجای نخ (در قضیه ی پردازنده) ، عبارت "هسته ی منطقی" را استفاده کن .
هر هسته ی منطقی ، در یک زمان ، یک نخ را میتونه اجرا کنه . البته اگه 2 هسته ی منطقی ، درون یک هسته ی فیزیکی باشن (مثل همین پردازنده ای که گفتی) ، اگه از اطلاعات مشترک بخوان استفاده کنن ، در اون زمان ، فقط یک هسته ی منطقی میتونه کد را اجرا کنه .






حالا می خوام چند تا برنامه به طور همزمان باز کنم و این برنامه ها به فرض رندر می کنن.


رندر ، کلمه ای هه که باز هم برای پردازنده معنا نداره . در سطح برنامه و نرم افزار معنا داره .
پردازنده فقط کد را اجرا میکنه .




حالا این نرم افزارها و عمل رندر به کدوم یک از روش هایی که میگم انجام می شه. این حالت ها برای نرم افزارهای 1 تردی یا دو تردی فرق دارن


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




الف) نرم افزار اول باز شده و در حال رندر کردنه و 2 هسته از سی پی یو رو اشغال می کنه و از هر هسته فقط 2 گیگاهرتز اشغال می کنه. بعد نرم افزار دوم رو باز می کنم و اون هم به دو هسته نیاز داره و چون از هر یک از دو هسته اول یک گیگاهرتز باقی مونده اول اون دو تا یک گیگاهرتز از هتسه اول رو اشغال می کنه و باقی پردازش میره روی هسته های 3 و 4 و همینطور که نرم افزارهای دیگه رو باز می کنم هسته های بعدی درگیر میشن تا 6 هسته پر بشه و 100 درصد پردازنده اشغال بشه


ب) نرم افزار اول باز شده و در حال رندر کردنه و پردازش روی 6 هسته پخش می شه و همینطور که نرم افزارهای بعدی که باز می کنم پردازش اونا هم روی این 6 هسته پخش می شه و به طور کلی این 6 هسته کم کم پر می شه تا 100 درصد پردازنده اشغال بشه.


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

اما سوئیچ روی نخ های یک پروسه (بخاطر قضیه ای که توضیح داده بودم) ، سربار کمتری داره . سوئیچ در پردازنده های داره Hyper Thread و SMT ، سربار کمتر و بنابراین زودتر انجام میشه . بنابراین تا جایی که پردازنده میتونه ، نخ های یک پروسه را توی یک هسته ی فیزیکی انجام میده مگر اینکه دمای اون هسته بالا بره یا شرایط دیگه ای پیش بیاد تا سوئیچ کنه روی یه هسته ی دیگه . البته این کلمه ی "میتونه" که گفتم ، به معنای بازه ی زمانی خاصی نیست . یا به این معنا نیست که کل کدهای اون نخ را در اون هسته ی فیزیکی اجرا میکنه . این کلمه ی "میتونه" ، ممکنه حتی در حد چند میکرو ثانیه یا میلی ثانیه باشه حتی اگه کد بزرگی برای اجرا داشته باشه . یعنی مثلا اون نخ ممکنه بعد از چند میکروثانیه هم ، از یه هسته ی فیزیکی ، به هسته ی فیزیکی دیگه سوئیچ کنه .

هر هسته ی منطقی هم موقع اجرای کد ، تا حداکثر فرکانس تعیین شده ، کدها را اجرا میکنه .
این رو هم یادت باشه که چیزی که در Task Manager نشون میده که مثلا یک پروسه داره 100 درصد از کارکرد اون هسته ی منطقی را میگیره ، این طور نیست .
یک هسته ی منطقی تا زمانی که اطلاعات مورد نظرش در حافظه ی رجیستر وجود نداشته باشه ، در حال انتظار هست . بنابراین تا زمانی که اطلاعات توی حافظه ی کش یا مخصوصا توی حافظه ی رم وجود داشته باشه ، اون هسته ی منطقی در حال انتظار هست . اگه این انتظار طولانی باشه (مثلا اطلاعات در رم یا page file یا همون حافظه ی مجازی باشه) ، اون هسته ی منطقی ممکنه به نخ دیگه ای سوئیچ کنه .






س3) ما برنامه نویسی موازی داریم. این برنامه نویسی موازی در نرم افزار باعث می شه که عمل رندر با دو سی پی یو همزمان انجام بشه یا اگه سی پی یو 8 هسته ای داشته باشیم روی هر 8 هسته انجام می شه که شاید دیگه نشه بگیم موازی چون به جای دو هسته داره روی 8 هسته انجام می شه.


متوجه ی اون تیکه از صحبتت که رنگی کردم ، نشدم .
از برنامه نویسی موازی ، دقیق اطلاعی ندارم .
توی سی شارپ ، برنامه نویسی موازی که با کلاس Parallel انجام میشه ، فرق خاصی با کلاس Thread نداره جز اینکه کدنویسی اش بهینه تر شده و معمولا ، فقط اندکی (نه زیاده) کدهاش زودتر اجرا میشه .
البته خود مایکروسافت هم اعلام کرده که بجای کلاس Thread ، از کلاس Task استفاده کنیم که کدهاش هم بهینه تر هست و هم ساده تر .
درون Task ، از کلاس Thread (یا نخ Managed که همین کلاس در سی شارپ هست یا نخ Unmanaged که برای سیستم عامل هست) استفاده میشه .
البته خود کلاس Thread در سی شارپ که Managed هست هم از نخ Unmanaged که برای سیستم عامل هست ، استفاده میکنه .


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






س4) الان بیشتر پردازنده های اینتل مثلا 6 هسته و 6 ترد دارن ولی AMD 6 هسته و 12 ترد داره. و من در جایی خوندم که هر کدوم مزایا و معایبی دارن. بعضی جاها این دو ترد داشتن مهمه و بعضی جا ها تک ترد بودن. می خوم کمی در این رابطه توضیح بدی. اصلا اگه دو ترد داشتن خیلی خوبه چرا اینتل گیر داده به تک ترد روی بیشتر پردازنده هاش.


قطعا 2 هسته ی منطقی به ازای هر هسته ی فیزیکی داشتن (فناوری HT و SMT) (مثل Ryzen 3600) ، بهتر هه .
تک هسته ی منطقی به ازای هر هسته ی فیزیکی (مثل Ryzen 3500) ، برتری ای نداره .
اونی که شما میگید ، عملکرد یک هسته ی فیزیکی هست . نه عملکرد هسته ی منطقی که بنابراین تعداد هسته ی منطقی به ازای هر هسته ی فیزیکی مهم باشه .
چون خیلی از برنامه ها ، از یک نخ (منظورم نخ سنگینی که اجرا و پردازش سنگینی بخواد) و بنابراین از یک هسته ی منطقی استفاده میکنن ، واسه ی همین ، در خیلی از جاها ، عملکرد تک هسته ای را مد نظر قرار میدن .

عملکرد تک هسته ای ، بستگی به نوع برنامه نویسی داره . اگه در برنامه ای که از یک نخ استفاده شده باشه ، معمولا دیگه HT و SMT ، کارایی چندانی نداره . در این صورت فرکانس اون هسته هست که سرعت تک هسته را مشخص میکنه . در این جور مواقع ، فرکانس turbo boost ، بسیار تاثیر گذار هست .
اگه برنامه نویسی در چند نخ انجام بشه ، بسته به این داره که سیستم عامل و پردازنده ، نخ های مختلف را در هسته های منطقی ای که در هسته های فیزیکیِ مجزا قرار دارن یا نه ، اجرا کنن ، عملکردشون هم متفاوت خواهد بود .

چون اجرای دو نخ از یه پروسه در دو هسته ی منطقی ای که در یک هسته ی فیزیکی قرار دارن ، باعث میشه سوئیچ نخ ها در این نوع هسته های منطقی سریعتر انجام بشه اما در عوض در مواردی که حافظه ی اشتراکی بطلبه ، قطعا چون یک هسته ی منطقی در اون زمان میتونه کد را اجرا کنه ، سرعتش از این لحاظ کم میشه اما در هسته های منطقی ای که در هسته های فیزیکی مجزا قرار دارن ، برعکسِ این حالت هست .

اما فکر کنم سیستم عامل یا پردازنده ، میسنجن که اگه اون نخ ، کد سنگینی (یا بهینه ای) بود که اجراش طولانی باشه ، در هسته ی منطقی ای که در هسته ی فیزیکی مجزا قرار داره ، اجراش کنن وگرنه (اگه کدهای نخ ، سبک باشه) در هسته ی منطقی ای که درون یک هسته ی فیزیکی قرار داره ، اجراش کنن (که در این گزینه ی دوم ، سوئیچ سریعتر ، مهمتر هه)





س5) پردازش در سی پی یو ها به صورت صف عمل می کنه یا پشته.


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



س6) خیلی از ویژگی هایی که در پردازنده های اینتل وجود داره در پردازنده های AMD نیست یعنی کاربردی ندارن. (منظورم از ویژگی ها مثلا SSE4.2 هست و امثال این ها )



س7) اگه معماری پردازنده ها رو در نظر بگیریم چه فرقی بین نسل 3000 رایزن و نسل 9 اینتل وجود داره. عمل پردازش اینا به یک شکله یا هر کدوم به شیوه خودشون پردازش می کنن. چون در خیلی جاها من دیدم که اینتل با تعداد هسته کمتر قدرتی تقریبا برابر AMD داره( به طور کلی میگم). کلا یک مقایسه ای وجود داره که بفهمیم معماری که اینتل در پردازنده هاش داره بهتره یا AMD در آخرین نسل.

اگه هر توضیح دیگه ای دارین که به روشن شدن مطلب کمک می کنه یا هر اطلاعات دیگه ای ممنون میشم که راهنمایی کنید.


درباره ی دستورالعمل ها ، اطلاع خاصی ندارم .
ان شاء ا... موفق باشی :11():