آشنایی با مفاهیم Thread ،Process و نحوۀ کارکرد Multithreading

آشنایی با مفاهیم Thread ،Process و نحوۀ کارکرد Multithreading

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

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

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

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

به طور کلی، یک سیستم‌عامل را می‌توان به منزلۀ ظرفی در نظر گرفت که در آن پراسس‌های متعددی با به‌کارگیری ریسورس‌های مورد نیازشان اجرا می‌شوند و هر پراسس نیز همچون ظرفی دارای چندین تِرِد است که هر یک مسئول انجام تَسک‌های زیرشاخۀ مربوط به آن پراسس اصطلاحاً Parent (والد) خود می‌باشند که در این مقاله قصد داریم تا روی مفهوم تِرِدها تمرکز کرده و به بررسی نحوۀ عملکرد آن‌ها بپردازیم.

آشنایی با تفاوت‌های مابین Process و Thread

سیستم‌عامل‌ها به منظور اجرای هر برنامه یا به اصطلاح Process (فرآیند) بخش جداگانه‌ای از حافظه‌ را اختصاص می‌دهند و به طور پیش‌فرض این حافظه را نمی‌توان با فرآیندهای دیگر به اشتراک گذاشت به طوری که مثلاً برنامۀ مربوط به مرورگر به حافظۀ اختصاص داده‌شده به نرم‌افزار مدیاپلیر دسترسی ندارد و بالعکس مضاف بر اینکه دو نمونۀ فرآیند مشابه یا به عبارتی دو بار اجرای برنامه‌ای یکسان را می‌توان مد نظر قرار داد به طوری که سیستم‌عامل با هر یک از آن‌ها به عنوان یک فرآیند مستقل رفتار کرده و بخش جداگانه‌ای در حافظه را به هر یک اختصاص می‌دهد (به طور پیش‌فرض دو یا چند فرآیند مستقل هیچ راهی برای به اشتراک گذاشتن داده‌های حاصل از اجرای برنامه‌ها با هم ندارند اما این در حالی است که با به‌کارگیری روش‌های پیشرفته‌ای تحت عنوان Inter Process Communication یا به اختصار IPC می‌توان ارتباطِی مابین فرآیندهای مختلف در سیستم‌عامل برقرار کرد.)

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

از جمله دیگر تفاوت‌های مابین تِرِد و پراسس می‌توان بدین موضوع اشاره کرد که تِرِدها روشی جهت ایجاد امکانِ اجرای چندین عملیات به صورت هم‌زمان برای برنامۀ مد نظر در سیستم‌عامل هستند. در واقع، بدون تِرِدها برنامه‌نویسان مجبورند تا به ازای هر یک از تَسک‌های مد نظر خود یک برنامۀ جداگانه نوشته و آن‌ها را اجرا کنند و در نهایت هر یک از فرآیندهای اجراشده را از طریق سیستم‌عامل با یکدیگر سینک کنند که هندل کردن این موضوع از طریق برقراری ارتباطاتی مثلاً از نوع IPC امکان‌پذیر بوده اما تا حدودی دشوارتر و همچنین کُندتر از انجام تَسک‌های مربوطه با به‌کارگیری تِرِدها می‌باشد چرا که اجرای پراسس‌های مختلف بدون تِرِدها و در نهایت سینک کردن آن‌ها با یکدیگر نیاز به دریافت ریسورس‌های بیشتری از سیستم‌عامل دارند و از همین روی گفته می‌شود که اصطلاحاً سنگین‌تر از تِرِدها هستند.

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

Green Thread چیست و چه کاربردهایی دارد؟

Green Thread تِرِدی است که قابلیت اجرای هم‌زمان برخی تَسک‌ها با به‌کارگیری تِرِدهای مختلف در طِی یک فرآیند والد را در پلتفرم‌هایی فراهم می‌کند که به صورت نِیتیو امکان اجرای آن‌ها را به صورت اصطلاحاً مالتی‌تِرِد ندارد. برای مثال، در یک ماشین مجازی که سیستم‌عامل مربوطه امکان پشتیبانی از اجرای برنامه‌ها به صورت مالتی‌تِرِد را ندارد، می‌توان با به‌کارگیری مفهوم Green Thread قابلیت اجرای هم‌زمان تَسک‌های مختلف را پیاده‌سازی کرد.

لازم به یادآوری است که نام‌گذاری این نوع تِرِدها تحت عنوان Green Thread برگرفته از نام تیمی تحت عنوان Green Team در شرکت Sun Microsystem است که این تیم لایبرری اصلی مربوط به تِرِدها در زبان برنامه‌نویسی جاوا را در دهۀ 90 طراحی کرده‌اند؛ به علاوه اینکه زبان‌های برنامه‌نویسی دیگری همچون Go ،Haskell یا Rust و غیره نیز مالتی‌تِرِدینگ را با به‌کارگیری این مفهوم به جای قابلیتی نِیتیو در این زبان‌ها معادل‌سازی کرده‌اند (امروزه زبان جاوا از فیچر Green Thread به منظور اجرای مالتی‌تِرِد برنامه‌های نوشته‌شده با این زبان استفاده نمی‌کند بلکه مالتی‌تِرِدینگ در سال 2000 به قابلیتی نِیتیو در این زبان تبدیل شده است.)

اجرای فرآیندها به صورت Multithread چه مزایایی دارا است؟

برای درک بهتر این موضوع، ابتدا بدین پرسش پاسخ می‌دهیم که «چرا نیاز داریم تا یک پراسس را با به‌کارگیری چندین تِرِد اجرا کنیم؟» که در پاسخ باید گفت همان‌طور که قبلاً اشاره کردیم، انجام برخی تَسک‌ها با به‌کارگیری قابلیت مالتی‌تِرِدینگ منجر به اجرای آن‌ها به صورت موازی شده و در نتیجه سرعت انجام کارها در سیستم‌های کامپیوتری افزایش می‌یابد. برای مثال، چنانچه بخواهیم فیلمی را با استفاده از نرم‌افزار مخصوص این کار ویرایش کنیم، می‌توان این کار را به صورت مالتی‌تِرِد انجام داده و عملیات مربوطه را در میان تِرِدهای مختلف پخش کرد به طوری که هر تِرِد یک قسمت از فیلم نهایی را پردازش کند. در چنین شرایطی، چنانچه ویرایش فیلم را با یک تِرِد انجام دهیم ممکن است فرآیند ویرایش ده‌ها دقیقه به طول انجامد اما این در حالی است که انجام چنین کاری با به‌کارگیری دو تِرِد ممکن است مثلاً 30 دقیقه، چهار تِرِد 15 دقیقه و به همین ترتیب کاهش یابد.

روی هم رفته می‌توان گفت که مالتی‌تِرِدینگ در همۀ موارد منجر به بهبود پرفورمنس در اجرای فرآیندهای مد نظر نمی‌شود به طوری که دولوپرها جهت انجام تَسک‌ها با به‌کارگیری قابلیت مالتی‌تِرِدینگ می‌باید شرایطی را مد نظر قرار دهند که برخی از مهم‌ترین آن‌ها عبارتند از:

- برای اجرای تمامی برنامه‌ها نیاز به استفاده از قابلیت مالتی‌تِرِدینگ نیست. در واقع، اگر اپلیکیشن مد نظر یکسری دستور را به صورت پشت سر هم انجام می‌دهد یا معمولاً منتظر پاسخ کاربر به منظور انجام برخی کارها می‌ماند، در چنین شرایطی مالتی‌تِرِدینگ نمی‌تواند مفید واقع گردد.

- صرفاً با افزودن تعداد تِرِدهای بیشتر به برنامه نمی‌توان سرعت اجرای آن را افزایش داد بلکه هر یک از تَسک‌های زیرشاخه نیز باید به دقت تعریف و طراحی شوند تا عملیات مربوطه به صورت موازی اجرا شده و در نهایت منجر به افزایش سرعت اجرای آن شوند.

- نمی‌توان گفت که به‌کارگیری تِرِدها در اجرای برخی عملیات حتماً منجر به اجرای موازی آن‌ها می‌گردد بلکه این موضوع به زیرساخت‌های سخت‌افزاری سیستم مورد استفاده نیز وابسته است.

درآمدی بر تفاوت‌های Concurrency و Parallelism

نکتۀ قابل‌توجه در ارتباط با سیستم‌عامل‌های مختلف این است که چنانچه سیستمی امکان پشتیبانی از اجرای چندین عملیات به صورت Parallel (موازی) را نداشته باشد، می‌توان چنین قابلیتی را در آن‌ها شبیه‌سازی کرد بدین صورت که مفهوم Concurrency (هم‌زمانی) را در اجرای عملیات مختلف مورد استفاده قرار داد. در واقع، در سیستم‌عامل‌هایی که امکانات سخت‌افزاری به منظور پشتیبانی از اجرای تَسک‌ها به صورت موازی را ندارند، می‌توان با به‌کارگیری برخی قابلیت‌های سخت‌افزاری همچون توانایی‌های سی‌پی‌یو عملیات مد نظر را به صورت هم‌زمان اجرا کرد که در ادامه به بررسی تفاوت دو مفهوم Concurrency و Parallelism می‌پردازیم:

- Concurrency به مفهومی اشاره دارد که در آن پروسۀ شروع، اجرا و تکمیل تَسک‌های زیرشاخه در عملیات مد نظر در چند بازۀ زمانی یکسان و معمولاً روی به اصطلاح Single-core Processors (پردازنده‌های تک‌هسته‌ای) انجام می‌گیرد.

- Parallelism به اجرای موازی دو یا چند عملیات به صورت هم‌گام و هم‌زمان اشاره دارد بدین معنی که اجرای آن‌ها به صورت هم‌زمان و روی پردازنده‌های چندهسته‌ای انجام می‌شود به طوری که هر یک از هسته‌ها به صورت جداگانه تَسک‌های زیرشاخۀ مد نظر را به صورت موازی و هم‌گام با یکدیگر اجرا می‌کنند.

به طور کلی، Concurrency حالتی کلی از Parallelism بوده و می‌توان از آن به منظور بهبود سرعت در اجرای تَسک‌های مختلف به صورت کانکارنت روی سیستم‌عامل‌هایی استفاده کرد که قابلیت پشتیبانی از اجرای تَسک‌های مذکور به صورت موازی را ندارند که در ادامه به بررسی نحوۀ اجرای عملیات هم به صورت موازی و هم کانکارنت روی سیستم‌عامل‌های مختلف می‌پردازیم.

آشنایی با نحوۀ اجرای فرآیندها به صورت Concurrent و Parallel

Central Processing Unit یا به اختصار CPU در سیستم‌های کامپیوتری مسئولیت پردازش داده‌ها را بر عهده دارد و از چند بخش تشکیل شده است که بخش اصلی آن تحت عنوان Core (هسته) شناخته می‌شود که انجام محاسبات مورد نیاز به منظور پردازش داده‌ها بر عهدۀ این بخش می‌باشد مضاف بر اینکه بخش هسته در سی‌پی‌یو در هر لحظه قادر بر اجرای تنها یک عملیات است.

البته موضوع فوق‌الذکر که در آن هر یک از هسته‌های پردازنده قادر بر اجرای تنها یک عملیات در هر لحظه می‌باشند، یک مشکل بزرگ بوده و از همین روی تکنیک‌های پیشرفته‌ای در سیستم‌عامل‌ها به منظور فراهم کردن قابلیت اجرای چندین فرآیند (یا تِرِدهای زیرشاخه) به صورت هم‌زمان در پردازنده‌ها و حتی در دیوایس‌های تک‌هسته‌ای (به ویژه در محیط‌های گرافیکی) توسعه داده شده‌اند که از جمله مهم‌ترین تکنیک‌ها برای این منظور می‌توان اصطلاحاً Preemptive Multitasking را نام برد که در آن پردازندۀ سیستم قابلیت اجرای فرآیندها با به‌کارگیری تِرِدهای مختلف را به صورت پیشگیرانه دارا است بدین معنی که در صورت لزوم می‌تواند اجرای برخی از تَسک‌ها را متوقف کرده و تِرِد مد نظر را به اجرای تَسکی دیگر اختصاص دهد سپس اجرای تَسک اول را در زمانی دیگر مجدداً از سر بگیرد.

بنابراین اگر سیستمی دارای پردازنده‌ای تک‌هسته‌ای باشد، سیستم‌عامل مد نظر قدرت محاسباتی این هسته را مابین چند تِرِد جهت اجرای تَسک‌های زیرشاخه یا چند پراسس مختلف توزیع می‌کند تا هر یک از تَسک‌ها یا فرآیندها پس از یکدیگر و در یک حلقه اجرا شوند.

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

همچنین سیستم‌عامل‌ها قادر به شناسایی تعداد هسته‌های سی‌پی‌یو و اختصاص فرآیندها یا تِرِدهای مورد نظر به هر یک از آن‌ها می‌باشند. در واقع، سیستم‌عامل بر اساس یکسری الگوریتم خاص و با بررسی شرایط هر یک از هسته‌های پردازنده تِرِدهای مربوطه را به منظور اجرای تَسک مد نظر و طبق زمان‌بندی مربوط به الگوریتم مذکور به آن‌ها اختصاص می‌دهد تا هر یک از تَسک‌ها با به‌کارگیری ریسورس‌ها و همچنین قابلیت‌های هستۀ متناظرشان اجرا شوند. به علاوه اینکه چنانچه تمامی هسته‌ها مشغول به اجرای تَسکی باشند، الگوریتم مالتی‌تَسکینگ پیشگیرانه انجام شده و بدین ترتیب برخی از تِرِدها از تَسک‌های متناظر گرفته شده و به انجام تَسکی دیگر اختصاص داده می‌شوند که در این بین پس از گذشت زمان معینی و بالتبع تکمیل تَسک مد نظر، تِرِد مذکور مجدداً به منظور از سر گرفتن اجرای تَسک قبل آزاد می‌گردد که در چنین شرایطی می‌توان گفت که سیستم‌عامل مد نظر توانایی اجرای فرآیندها یا تِرِدهایی بیش از ظرفیت تعداد هستۀ موجود در سیستم‌عامل را دارا است.

نحوۀ اجرای برنامه‌ها به صورت Multithread در یک سیستم‌عامل تک‌هسته‌ای

همان‌طور که پیش‌تر اشاره کردیم، اجرای تَسک‌ها به صورت موازی در پردازنده‌های تک‌هسته‌ای امکان‌پذیر نمی‌باشد اما این در حالی است که در صورت لزوم می‌توان عملیات مد نظر را به صورت مالتی‌تِرِد در آن‌ها اجرا کرد. در واقع، هنگامی که فرآیندی با چند تِرِد اجرا می‌شود، به‌کارگیری الگوریتم مالتی‌تَسکینگ پیشگیرانه منجر به اجرای برنامه به صورت کانکارنت می‌شود به طوری که حتی ممکن است اجرای برخی تَسک‌های زیرشاخه کُند بوده یا برخی از آن‌ها جهت اختصاص تِرِد متناظرشان به تَسک‌های دیگر مسدود شوند اما به هر حال برنامۀ مد نظر در حال اجرا باقی مانده و متوقف نمی‌شود.

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

حال فرض کنید که چنین برنامه‌ای را به صورت مالتی‌تِرِد نوشته باشیم که در آن مثلاً تِرِد الف مسئول دسترسی به هارد بوده و تِرِد ب مسئول پردازش‌های مربوط به اینترفیس است. در این مثال، اگر اجرای تِرِد الف به دلیل سرعت کُند سیستم دچار مشکل شود، تِرِد ب همچنان می‌تواند فرآیندهای مربوط به اینترفیس را هندل کند و این امر منجر بدین می‌شود که برنامۀ مد نظر توانایی پاسخگویی به سایر درخواست‌های کاربر را داشته باشد. در واقع، این روش بدین صورت کار می‌کند که سیستم‌عامل مربوطه دائماً مابین تِرِدهای مذکور سوئیچ کرده و بدون آنکه از سرعت اجرای آن‌ها کاسته شود، در هر مرتبه ریسورس‌های سی‌پی‌یو را متناسب با نیاز هر یک از تَسک‌های زیرشاخه به تِرِد متناظرشان اختصاص می‌دهد.

چگونه قابلیت Multithreading منجر به بروز مشکل در اجرای تَسک‌های برنامه می‌شود؟

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

در برخی موارد دسترسی دو یا چندین تِرِد به یک حافظۀ مشترک مشکلی را به وجود نمی‌آورد اما این در حالی است که مشکلات زمانی رخ می‌دهند که حداقل یکی از تِرِدهای مذکور وظیفۀ اصطلاحاً Write (نوشتن) در حافظۀ مشترک را داشته و تِرِدی دیگر تَسکی مربوط به Read (خواندن) از آن را بر عهده داشته باشد که در چنین شرایطی حداقل دو مشکل می‌تواند رخ دهد که عبارتند از:

- Data Race: شرایطی را در نظر بگیرید که در آن تِرِد Writer در حال نوشتن دیتای مربوطه در حافظه بوده و محتوای آن را تغییر می‌دهد و از سوی دیگر تِرِد Reader نیز به صورت هم‌زمان به حافظه دسترسی داشته و اطلاعات را از آن می‌خواند که در این صورت چنانچه تَسک مربوط به تِرِد Writer به اتمام نرسیده باشد، بدیهی است که تِرِد Reader به اطلاعاتی ناقص یا اشتباه دسترسی پیدا می‌کند.

- Race Condition: حال فرض کنیم که یک تِرِد صرفاً پس از اتمام تَسک مربوط به تِرِد دیگری، اجازۀ دسترسی به هارد را داشته باشد که در چنین شرایطی اتفاقی به مراتب بدتر از دسترسی به دیتای ناقص یا اشتباه رخ می‌دهد! بدین ترتیب که ممکن است مثلاً تِرِد Reader یا Writer تَسک متناظرشان را بر اساس ترتیبی غیرقابل‌پیش‌بینی انجام داده و بدین ترتیب هرگز اجازۀ دسترسی به هارد را به تِرِد دیگر ندهند که در چنین شرایطی با مشکلی تحت عنوان Race Condition مواجه می‌شویم. در واقع، Race Condition زمانی اتفاق می‌افتد که تَسک‌های مد نظر به همان ترتیب مورد انتظار اجرا نشده و منجر بدین می‌شود تا تِرِدهای مربوطه هرگز اجازۀ دسترسی به حافظه به منظور انجام تَسک متناظر را نداشته باشند و در صورتی می‌توان از وقوع این شرایط بحرانی جلوگیری کرد که اطمینان حاصل کنیم تا تَسک‌های مد نظر حتماً بر اساس ترتیبی مشخص اجرا می‌شوند.

بنابراین می‌توان گفت که حتی اگر بتوان از دسترسی به دیتای نادرست برخی تِرِدها در یک برنامه جلوگیری کرد، برنامۀ مذکور می‌تواند در شرایط بحرانی گیر کرده و اجرای آن با مشکل مواجه شود که در همین راستا و برای حل مشکلاتی از این دست مفهومی تحت عنوان Thread Safety را در ادامه معرفی خواهیم کرد.

آشنایی با مفهوم Thread Safety

در صورتی می‌توان گفت یک قطعه کد اصطلاحاً Thread Safe است که با وجود به‌کارگیری تِرِدهای بسیار به منظور اجرای آن، به درستی کار کند بدین معنی که هیچ گونه به اصطلاح Data Race یا Race Condition رخ ندهد. برای مثال، یکی از ویژگی‌های برخی لایبرری‌های Third-party اصطلاحاً Thread Safe بودن آن‌ها است بدین معنی که چنانچه دولوپرها اقدام به نوشتن برنامه‌ای با قابلیت مالتی‌تِرِدینگ با به‌کارگیری آن لایبرری کنند، تمامی فانکشن‌های تعریف‌شده در لایبرری مد نظر قابلیت فراخوانی و اجرا از طریق تِرِدهای مختلف بدون مواجه شدن با مسائل کانکارنسی را دارند که در ادامۀ این مقاله با برخی از دلایل اصلی این مشکلات آشنا خواهیم شد.

همان‌طور که پیش‌تر اشاره کردیم، هستۀ پردازنده قابلیت اجرای تنها یک تَسک را در آنِ واحد دارا است و از همین روی گفته می‌شود چنین عملیاتی به اصطلاح Atomic (غیرقابل‌تجزیه) هستند چرا که نمی‌توان آن‌ها را به تَسک‌هایی زیرشاخه تقسیم کرد و همین خصوصیت عدم تجزیه‌پذیری برخی فرآیندها منجر بدین می‌شود تا به صورت ذاتی Thread Safe باشند!

در واقع، نحوۀ کارکرد عملیات به اصطلاح اَتمیک بدین صورت است که وقتی یک تِرِد تَسکی غیرقابل‌تجزیه به منظور نوشتن دیتایی روی داده‌های مشترک انجام می‌دهد، هیچ تِرِد دیگری قابلیت خواندن از روی این دیتای اصلاح‌شدۀ ناقص را ندارد و برعکس این موضوع نیز صادق است به طوری که وقتی یک تِرِد به صورت تجزیه‌ناپذیر تَسکی را به منظور خواندن از روی داده‌های ذخیره‌شده در حافظۀ مشترک انجام می‌دهد، تمام مقادیر داده‌ای را در همان لحظه و به صورت یکجا می‌خواند؛ به عبارت دیگر، در عملکرد تِرِدهای مربوط به تَسک‌های اَتمیک اشتباهی رخ نداده و بدین ترتیب مسئله‌ای همچون Data Race در طِی دسترسی آن‌ها به حافظۀ مشترک اتفاق نمی‌افتد.

اما در عین حال لازم به یادآوری است که برخی کارها در سیستم‌های کامپیوتری به صورت اَتمیک انجام نمی‌شوند به طوری که حتی یک دستور انتساب ساده همچون x = 1 در برخی از سخت‌افزارها ممکن است از چندین دستور متعدد اَتمیک تشکیل شده باشد و این در حالی است که فرآیند انتساب عدد به آن متغیر به عنوان یک دستور غیر اَتمیک انجام می‌شود و ممکن است در این حین مسئلۀ Data Race اتفاق بیفتد بدین صورت که مثلاً یک تِرِد متغیر x را خوانده و تِرِدی دیگر در حال انتساب مقدار به آن باشد!

به‌کارگیری قابلیت Preemptive Multitasking که پیش از این با مفهوم‌اش آشنا شدیم در سیستم‌عامل‌ها اجازۀ کنترل کامل بر مدیریت روند اجرای تَسک‌های زیرشاخه از طریق تِرِدها را به سیستم‌عامل مد نظر می‌دهد بدین صورت که می‌توان با به‌کارگیری یکسری الگوریتم پیشرفتۀ زمان‌بندی، مسائلی همچون امکان شروع، توقف و خاتمۀ اجرای عملیات از طریق آن‌ها را کنترل کرد. در واقع، دولوپرها نمی‌توانند بدون استفاده از قابلیت Preemptive Multitasking زمان یا ترتیب اجرای دستورات برنامه را کنترل کنند. برای مثال، کد فرضی زیر را مد نظر قرار می‌دهیم:

writer_thread.start()
reader_thread.start()

در کد فوق، نمی‌توان با اطمینان کامل گفت که دستورات مربوطه دقیقاً بر اساس ترتیب تعیین‌شده اجرا می‌شوند به طوری که ممکن است در هر بار اجرا رفتاری متفاوت را از خود نشان داده و در برخی مواقع تِرِد رایتر در ابتدا اجرا شود یا در اجراهای دیگر تِرِد رایتر پس از تِرِد ریدر اجرا گردد. حال در صورتی که بخواهیم مثلاً در برنامۀ فوق تِرِد رایتر همواره قبل از تِرِد ریدر اجرا شود، بدین ترتیب می‌توان گفت برنامۀ مد نظر در طِی اجرای تَسک‌های زیرشاخه ممکن است دچار مسئلۀ Race Condition شود چرا که رفتاری نامشخص و غیرقابل‌پیش‌بینی داشته و نتیجۀ آن در هر بار اجرا تغییر می‌کند و نیاز به توضیح نیست که دیباگ کردن چنین برنامه‌هایی که تحت تأثیر شرایطی بحرانی از این دست هستند بسیار پیچیده است چرا که همیشه نمی‌توان ارور مربوطه را به صورت کنترل‌شده بازآفرینی کرده و به دنبال دیباگ کردن آن بود!

جمع‌بندی
در این مقاله به بررسی مفهوم تِرِد پرداخته و دیدیم که چگونه می‌توان با تقسیم فرآیندهای مختلف به یکسری تَسک زیرشاخه آن‌ها را در سیستم‌عامل‌های تک‌هسته‌ای به صورت کانکارنت و همچنین در سیستم‌های چندهسته‌ای به صورت موازی اجرا کرد. از سوی دیگر، به بررسی مسائل مربوط به اجرای برنامه‌ها به صورت کانکارنت پرداخته و مشکلاتی را برشمردیم که ممکن است در طِی اجرای عملیات مد نظر به صورت کانکارنت با آن‌ها مواجه شویم و در نهایت هم روشی را به منظور مواجهه با چنین مشکلاتی ارائه دادیم.

از بهترین نوشته‌های کاربران سکان آکادمی در سکان پلاس


online-support-icon