چگونه فرایند چندریسمانی یا چندنخی (Multi Threading)، سبب بهبود فرایند ساخت یک بازی ویدیویی میشود؟
فرض کنید در یک پارک مشغول قدم زدن هستید. هوای خنکی پوست شما را لمس میکند و خورشید با متوسط فاصله ۱۵۰ میلیون کیلومتری، پس از ۸ دقیقه و بیست ثانیه نور خود را به زمین ساطع میکند. این نور، که خود تبدیل به نور مرئی، مادون قرمز و فرابنفش میشود، توسط اجسام جذب شده و بعدها به صورت یک انرژی گرمایی تبدیل و یک آبوهوای دلچسب را پدید میآورد. به قدم زدن ادامه دهیم. کودکان مشغول بازی، دو پیرمرد در حال تعریف خاطرات گذشته، صدای فروشنده بستنی، دورهمی دانشجویان و هزاران فعالیت دیگر که در جریان است. همهچیز به نظر عادی میرسد تا اینکه شما از خود میپرسید: چگونه این همه فعالیت (دیداری، شنیداری و غیره)، توسط مغز به صورت همزمان پردازش میشوند؟ شما قدم میزنید و در همین حالت قادر به شنیدن صدا و مشاهده دیگران هستید. چنین مسئلهای برای ماشین و سامانههای دیجیتال نیز برقرار است، اما در پیادهسازی بسیار پیچیدهتر هستند. یک سیستم باید همزمان به چندین فعالیت مختلف بپردازد. اصلا چگونه باید یک نظم یا الگوریتم تشکیل داد؟ براساس اولویت باشند؟ آن دست موارد که نیاز به منابع و زمان طولانی ندارند؟ اگر تداخلی رخ دهد چطور؟
در این نوشته قصد داریم به یک مفهوم کلیدی در جهان بازیسازی، یعنی چند ریسمانی، بپردازیم.
یک مغز دیجیتال
پایهترین دستگاه دیجیتال نیز باید عملکردی نسبتا هوشمندانه از خود نشان دهد. در دل دستگاههای دیجیتال عضو حیاتی به نام CPU وجود دارد که در واقع همان مغز سیستم به شما میرود. قصد دارید یک پوشه جدید بسازید؟ یک موسیقی پخش کنید؟ چنین چیزی نیاز به فهم ندارد؟ این CPU است که تمامی درخواستهای کاربر را اجرا میکند. اگر شما درک درستی از این بخش حیاتی داشته باشید، با یک مطالعه ساده و بدون نیاز به مقاطع عالی حوزه علوم کامپیوتر، این امکان وجود دارد تا بهترین عملکرد و ارتباط را ایجاد کنید. تمام تلاش در علوم کامپیوتر شاید در همین تعامل خلاصه شود. شما قصد دارید چندین فعالیت مخلتف را به مانند، پخش موسیقی، ویرایش سنگین یک عکس در برنامه ادوبی و چند فعالیت دیگر را انجام دهید. بله ممکن است شما از پردازنده خوبی برخوردار باشید و تمامی این موارد حتی در صورتی که مسئولیتهای بسیاری به دوش CPU باشد، در نهایت انجام شود.
CPU از اجزای بسیاری تشکیل شده
اما از سوی دیگر شما به تعامل با CPU اهمیت میدهید و سعی دارید بجای اذیت کردن سیستم خود، با آن تعامل داشته باشید. همانطور که در نوشتههای قبلی اشاره کردیم و باز هم اشاره میکنیم، زبان تعامل میان انسان و کامپیوتر یا سیستمهای دیجیتال، باینری است! یعنی صفر و یک. خود CPU از واحدهای مختلفی تشکیل میشود. در بخش ریاضی و عملیاتهای منطقی، جمع، تفریق، یا حتی مقایسه دو عدد، وظیفه با (Arithmetic Logic Unit) ALU است. در واحد کنترل (Control Unit)، تشخیص و تصمیمات صورت میگیرد. فایل باید به کجا برود؟ داده در کدام بخش باید پردازش شود؟ در بخشی به نام رجیسترها (Registers)، این حافظههای کوچک و فوقالعاده سریع، با دسترسی به دادههای ضروری و فوری سرعت عمل را بالاتر میبرند.
فکر میکنم تا حدودی موضوع روشن شده باشد که تمامی درخواستهای شما توسط این بخش به عمل تبدیل میشود. با فشردن یک دکمه در باز کارکتر حرکت میکند، صدای شلیک یک گلوله از دور شنیده میشود و مجموعهای از این دستورالعملهای متفاوت. حال اگر تداخلی در این مجموعه رخ دهد، یعنی فرض داشته باشیم ALU در مقابل محاسبات بسیاری قرار گیرد، چه اتفاقی رخ خواهد داد؟ در ویدئوگیم امروزی قرار نیست با تعدادی دستورالعمل ساده مواجه شویم، بلکه هوشمصنوعی، فیزیک اجسام، صدای محیط و موارد بسیاری دیگر باید به صورت هماهنگ اجرا شوند چرا که در غیر این صورت با دادههای آشفتهای روبهرو هستیم.
تعداد هستههای یک CPU
تا اینجا امیدوارم توانسته باشیم مقداری از اهمیت و چگونگی کارکرد CPU را بسیار خلاصهوار توضیح داده باشیم. لازم به ذکر است که مبحث CPU در این مقاله تنها به صورت اختصار و جزئی معرفی میشود.
در دهههای گذشته ساختار CPU با تغییرات فراوانی مواجه بوده. آنها دیگر فقط یک واحد پردازنده نیستند، بلکه از چندین هسته پردازشی تشکیل شدهاند. در واقع هر هسته (Core)، را میتوان به نوعی یک مغز کوچک تعبیه شده در یک مغز بزرگتر دانست که هرکدام قادر به انجام وظایف مستقل خود هستند. شاید برای برخی عجیب باشد، اما قبل چنین پیشرفتی، CPUها فقط یک هسته داشتند، به منظوری دیگر این تک هسته برای چند برنامه در حال اجرا باید مدام خودش را جابهجا کند. پس برای توسعه یک بازی، پردازش فیزیکی، محاسبه موقعیت دشمن، بازخورد به فشردن یک دکمه باید پشتسر یکدیگر و البته با اولویتهای دقیق اجرا شود. سادهتر بگویم: محدودیتها بیشمارند و برنامهنویس باید تا حد ممکن کد خود را بهینه بنویسد. با این وجود خبری از هوش مصنوعی، محیطهای پویا و فیزیک واقعگرایانه نخواهد بود.
چند ریسمانی و تک ریسمانی. تفاوت کاملا قابل مشاهده است. در یک برنامه تک ریسمانی همه وظایف به گردن یک ریسمان و بالعکس در یک برنامه چند ریسمانی، این وظایف تقسیم میشوند
با ورود CPUهای چند هستهای، امکان پردازش موازی فراهم شد. اکنون برنامه نویسان میتوانند چند عملیات مختلف را به طور همزمان و بهتر از گذشته اجرا کنند. یعنی یک هسته مربوط به فیزیک، یک هسته دیگر مربوط به منطق بازی و این عملیاتهای مختلف بین هستهها تقسیم شوند. با این حال این مربوط به بستر سختافزار است و برنامهنویسان باید به خوبی تقسیم وظایف کنند. چنانچه این ظایف به درستی انجام نشود، ممکن است تمامی وظایف برگردن یک هسته و عملا بازی دیگر نمیتواند بهینه باشد! یعنی اگر ما ۷۷ هسته دیگر هم داشته باشیم و برنامهنویس نتواند به خوبی از آن استفاده کند، عملا هیچ فایدهای ندارد چرا که تعداد هسته بیشتر یک ظرفیت و البته یک بستر برای تیم توسعه است.
در بین تمامی ویژگیهایی که باید داشته باشید از جمله، دانش بسیار بالا در ریاضی (جبر و ریاضیات سهبعدی)، زبان ++C (معمولا تجربه چندساله)، سابقه ساخت یک اثر یا همکاری با یک استودیو، یک مورد بسیار مهم در این بین وجود دارد: تجربه و توانایی نوشتن کدهای Multithreading. هنگامی که درباره بازیهای مدرن صحبت میکنیم، مفهومی به چندنخی به دفعات بسیاری شنیده میشود. اما چند نخی چیست؟ چرا نسبت به گذشته از اهیمت بسیار بالایی برخودار شده؟ برای فهم این موضوع از واژه «ریسمان» شروع میکنیم.
نخ یا ریسمان (Thread)
ما متوجه شدیم که CPUهای تک هستهای عملا مانعی برای پرداختن به چند فعالیت متفاوت بودند. سوای از بازیسازی، نرمافزارهای قدیمی نیز در همین محدوده قرار داشتند. یک سیستم تک پردازنده باید تمام قدرت خودش را با سرعت هرچه تمامتر بین عملیاتهای مختلف تقسیم کند. خب، در یک بازی با این حجم از داده چه باید کرد؟ و نکته مهمتر اینکه چگونه این مسئله مهم را باید به گوش CPU برسانیم که عملا یک ماشین است؟ ابتدا باید به CPU بگوییم که تمامی فعالیتها به بخشهای کوچک تقسیم و بین آنها جابهجا شود تا عملا هیچ فعالیتی خاموش نماند! به این بخشهای کوچک تقسیم شده ریسمان میگویند. در تعریفی ساده، ریسمان یک مسیر اجرای مستقل درون یک برنامه است.
برنامهنویسی چند ریسمانی در پردازندههای تکهستهای نیز وجود داشت، اما مشکل اساسی به تعداد هستهپردازنده باز میگردد. برنامهنویسی چندریسمانی درواقع اجازه میدهد فرضا در یک مرورگر، یک فایل دانلود و چنانچه مایل باشید همزمان در آن مرورگر به یوتیوب رفته و یک ویدئو تماشا کنید. خب، اگر چند ریسمانی نباشد چطور؟ شما ابتدا باید فایل را دانلود کنید و بعد به سراغ تماشای ویدئو بروید. عملا شما حق دارید تنها یک درخواست داشته و برای درخواست بعدی منتظر بمانید. آیا این چیزی جز محدودیت نیست؟
بگذارید با یک مثال دیگر جلو برویم. شما یک برنامه نوشتهاید، وتمامی این خطوط برنامه قرار است، پشتسر یکدیگر اجرا شوند؛ دقت کنید پشتسر یکدیگر! اما من برنامه نویس قصد ندارم برنامه از خط یک به تا خط ۱۰ به صورت پیوسته اجرا شود، بلکه همهچیز خطوط به طور همزمان اجرا شوند. من قصد دارم برنامه واکنشپذیر و بسیار سریعتر باشد. اگر برنامه فتوشاپ برای ۱۰۰ قطعه عکس، رویهای تک نخی داشته باشد، باید برای هرعکس که تنها تغییرشان رنگ فرضا سفید به سیاه بود، کاربر ممکن است چندین ساعت در انتظار بماند.
مزایای استفاده از نخ:
استفاده از چند نخ باعث بهبود عملکرد میشود.برنامه نسبت به ورودی کاربر سریعتر واکنش نشان میدهد.سازماندهی بهتر وظایف، یعنی هر نخ مسئولیت یک کار خاص خواهد شد.
بد نیست بدانید:
CPU از معمولا از چند الگوریتم استفاده میکند:FCFS (First Come First Serve): اجرای به ترتیب فعالیتها.SJF (Shortest Job First): اجرای عملیاتی که منابع کمتری (سبکتر است) درخواست میکند.Round Robin: هر پردازش برای مدت کوتاهی اجرا میشود و بعد به سراغ فعالیت بعدی میرود.Priority Scheduling: پردازشها بر اساس اولویت انتخاب میشوند.هیچکس: با هوش مصنوعی دیگر هیچ مشکلی وجود ندارد. برنامهنویس (این میم تنها مختص برنامه نویسان با تجربه و حرفهای است!)
بله، استفاده از نخها باعث بهبود عملکرد خواهد شد اما خطرات و چالشهای بسیاری در پیدارند. یکی از این خطرات که در استفاده از نخها رخ میدهد، مربوط به تداخل نخها است. این معضل زمانی خودش را نشان میدهد که دو یا چند نخ قصد دارند در یک زمان، از یک داده مشترک استفاده کنند و ترتیب اجرای آنها مشخص نباشد. در چنین شرایطی امکان نتایج متفاوت وجود دارد، یعنی هر لحظه بازی، رفتاری غیرقابل پیشبینی و نادرست از خود نشان میدهد. اما کار به اینجا ختم نمیشود، چرا که مشکل بعدی مربوط به حل همین خطاهای بسیار خطرناکاند که اغلب در بازه زمانی خاص رخ و عیبیابی بسیار دشوار است. چالش بعدی مربوط به “قفل شدن متقابل” یا Dead Lock است. این اتفاق زمانی رخ میدهد که چند نخ برای ادامه کار خود نیازمند منابعی مشترکی هستند که توسط نخهای دیگر قفل شدهاند. بدین صورت هیچ ریسمانی نمیتواند به وظیفه خود برسد، زیرا منتظر یکدیگر هستند!
ایکاش مشکلات به همین دو مسئله ختم شود. مسئله مهم دیگر، پیچیدگی طراحی و نگهداری برنامههای چندریسمانی است. برخلاف برنامههای ساده که تنها یک مسیر مشخص و ساده در پیش دارند، در برنامههای چندریسمانی شما باید به این فکر کنید کدام ریسمانها همپوشانی دارند، کدام داده مشترک است، کدام ریسمان زودتر شروع و کدامین ریسمان پتانسیل خرابی دارد. و اما گل سرسبد اشکال زدایی یا Debugging. بسته به نوع برنامه و مقدار حجم کد و پیچیدگی این مورد تحت تاثیر قرار میگیرد، اما در چندریسمانی همانطور که گفتیم ممکن است در بازهای رخ دهد که حتی شاید در یک صحنه هیچ نشانی از خود نشان ندهد اما در اجرای دیگری ظاهر شوند.
تنها راکستار (Rockstar)، توان خلق چنین جهان پویایی را دارد
به جهان Red Dead Redemption 2 فکر کنید. سوار بر اسب شده و یک مسیر را طی میکنید. اسب با دستور و سرعت مد نظر حرکت میکند، نور میتابد، فیزیک اسب مثل زدنی است، صدای پرندگان به گوش میرسد، آب در جریان و آهویی که قصد رفع تشنگی دارد. به یک باره چند فرد ناشناس از راه میرسند، قصد و نیت آنها تا این لحظه قابل پیشبینی نیست اما به نظر هدف خوبی در سر ندارند. ناگهان متوجه میشوند که شما عضوی از گنگ وندرلیند هستید و بدون لحظهای درنگ اسلحه خود را بیرون آورده و شروع به تیراندازی میکنند. تُن موسیقی تغییر میکند، آب همچنان جریان دارد اما آهو با صدای شلیک برای نجات جان خود پا به فرار میگذارد. اسب ترس را بروز میدهد، آرتور برای دفاع از خود شروع به شلیک میکند. همه چیز در کسری از ثانیه تغییر کرد. چگونه این حجم از هماهنگی رخ میدهد؟
برنامهنویسی چند ریسمانی بدون دانستن چگونگی عملکرد CPU عملا هیچ کاربردی ندارد، زیرا تمام تلاش یک برنامهنویس در راستای اجرای سریع و بهبود عملکرد برنامهاش است.
باید یک ریسمان مربوط به ورودی کاربر باشد. شما یک دکمه را فشرده و انتظار دارید در زمان مناسبی شخصیت اصلی یا وسیله نقلیه واکنش نشان دهد. هر آنچه دارای هوشمصنوعی است باید فکر کند و تصمیم بگیرد. برخورد دو ماشین، فیزیک سلاح، سقوط یک شئ و دیگر موارد فیزیک که نیاز به پردازش دارند، اینها چیزی جز موارد کلی از یک ساختار بازی نیستند که باید در دستهایی از ریسمانها دسته بندی شوند. اگر چنین ساختاری در ریسمانهای مجزا قرار نگیرد، باید پشتسر هم صف بکشند در صورتی که هرکدام از این پردازشها نیاز به زمان دارند. همه چیز در جریان است و نباید اخلالی ایجاد یا دادهای مانع از این هماهنگی شود.
++C زبان قدرتمند اما سختگیری است
برنامه نویسی چند نخی تاریخچه طولانی دارد. در این بخش به دلیل استفاده این دو زبان (C و CPP) در بازیسازی، نگاهی کوتاه به آنها میاندازیم.
کتابخانه (Library) چیست؟کتابخانه در برنامه نویسی مجموعهای از کدهای از پیش نوشته شدهای است که اجازه تکرار کد یا دوباره نوشتن را نمی دهند. برای مثال یک کتابخانه به منظور استفاده مجدد از تمامی سلاحها. اینکه چند نوع یا چه نوع باشند.API (Application Programming Interface) چیست؟API یک مجموعه از قواعد و ابزارها که اجازه میدهد برنامههای مختلف بایکدیگر در ارتباط باشند. شما برای سفارش غذا به سراغ آشپز نخواهید رفت، بلکه گارسون را صدا میزنید!
زبان C در زمان خود نه تنها یک انقلاب بزرگ، بلکه پایهگذار بسیاری از زبانهای مدرن امروزی است. برنامه نویسی سیستمی (کامپایلرها)، ساخت سیستمعامل، سیستمهای نهفته، برنامه نویسی شبکه و سرور و بازیسازی، از حوزههایی هستند که این زبان میتواند قدرت خودش را نشان دهد. با این حال زبان C عاری از مشکل نبود؛ در ابتدای ظهور C این زبان به صورتی ذاتی برای برنامه نویسی چندریسمانی خلق نشده یا بهتر است بگوییم قابلیت پشتیبانی از این ویژگی را نداشت. برنامه نویسان مجبور شدند از کتابخانههای خارجی به مانند Posix Threads در سیستم عاملهای شبه یونیکس یا Windows Threads API استفاده کنند. این وابستگی موجب پیچیدگی بسیار بالا و البته انحصاری شدن سیستم عامل گشت. یعنی کدهای نوشته شده فرضا ویندوز، دیگر روی لینوکس قابل اجرا نبود. زمان گذشت و این شرایط سخت مقداری با معرفی استاندارد C11، قابل تحمل شد. C11 توانست با پشتیبانی از چندریسمانی هرچند محدود، از وابستگی خارج و کتابخانه خودش را داشته باشد.
نمونهای ساده از نحوه پیادهسازی یک برنامه چند ریسمانی
تا پیش از سال ۲۰۱۱ هیچ راه استانداردی برای نوشتن یک برنامه چندریسمانی در ++C وجود نداشت. ++C همچون پدر خود یعنی C، از همان کتابخانههای خارجی برای حل این موضوع استفاده میکرد. پس از ۱۱++C، این زبان به خودش اکتفا کرد، حالا زبان ++C قدرتمندتر گذشته بود. دیگر نیازی به پلتفرم خاصی نیست، بلکه تنها لازم است در برنامه خود از “std::thread” برای ساخت یک ریسمان استفاده کنید. همچنین قابلیتهایی چون “std::mutex” و “std::lock_guard” برای تداخل ریسمانها (صرفا برای بهبود عملکرد نه رفع کامل) وجود داشتند. بعدها با استانداردهای جدید به مانند: ۱۴++C تا به امروز یعنی ۲۳++C مشکلات یکی پس از دیگری رفع و ++C عملکرد بهتری را نسبت به گذشته ارائه میدهد.
از اینکه تا انتهای این نوشته بنده را همراهی کردهاید، سپاسگزارم. در این نوشته سعی شد تا حد ممکن مسائل ساده بیان شود تا تمامی مخاطبین با مفهوم چندریسمانی آشنا شوند. شما میتوانید برای کسب اطلاعات بیشتر به منابع بسیاری که در سرتاسر اینترنت وجود دارند رجوع کنید.
طراحی و اجرا :
وین تم
هر گونه کپی برداری از طرح قالب یا مطالب پیگرد قانونی خواهد داشت ، کلیه حقوق این وب سایت متعلق به وب سایت تک فان است
دیدگاهتان را بنویسید