تفاوت بین نخ معمولی و نخ مجازی در جاوا – از صفر تا صد


در این مقاله به بررسی تفاوتهای بین نخ معمولی و نخ مجازی در جاوا میپردازیم. سپس چندین کاربرد نخهای مجازی (Virtual Threads) و API-های مربوطه را معرفی میکنیم. بدین منظور از پروژه Loom (+) بهره خواهیم گرفت.
پیش از آغاز باید اشاره کنیم که این پروژه هم اینک در حال توسعه است. ما مثالهای خود را بنا بر دسترسی قبل از موعدی که به فایل این پروژه (openjdk-15-loom+4-55_windows-x64_bin) داشتیم اجرا میکنیم.
نسخههای جدیدتر این پروژه تغییر یافتهاند و با API-های کنونی سازگاری ندارند. معنی این حرف آن است که هم اینک تغییر عمدهای در API رخ داده است، چون کلاسی که قبلاً با نام java.lang.Fiber استفاده میکردیم، حذف شده و به جای آن با کلاس جدید java.lang.VirtualThread کار میکنیم.
مقایسه کلی نخ معمولی و نخ مجازی در جاوا
از دید سطح بالا نخ معمولی از سوی سیستم عامل، مدیریت و زمانبندی میشود، در حالی که نخ مجازی از سوی ماشین مجازی مدیریت و زمانبندی میشود. بنابراین برای ایجاد نخ کرنل جدید باید یک فراخوانی سیستمی داشته باشیم و این کار از نظر عملیاتی پرهزینه است.
به همین دلیل است که از «استخر نخ» (Thread Pool) به جای تخصیص مجدد و آزادسازی نخها در موارد نیاز استفاده میکنیم. به این ترتیب اگر بخواهیم اپلیکیشن خود را با افزودن نخهای بیشتر مقیاسبندی کنیم، به دلیل وجود چیزی به نام «سوئیچ زمینه» (context switching) و اثر آن بر حافظه، هزینه دستکاری این نخها ممکن است چشمگیر باشد و روی زمان پردازش تأثیر بگذارد.
بنابراین به طور معمول تمایلی به مسدودسازی این نخها نداریم و این کار موجب رواج استفاده از API-های نامسدودساز I/O و API-های ناهمگام میشود که ممکن است کد را شلوغ کنند.
از سوی دیگر نخهای مجازی از سوی JVM مدیریت میشوند. از این رو تخصیص آنها نیازمند یک فراخوانی سیستمی نیست و از سوئیچ زمینه سیستم عامل آزاد هستند. به علاوه نخهای مجازی روی نخ حامل اجرا میشوند که نخ واقعی کرنل است که در پسزمینه مورد استفاده قرار میگیرد. در نتیجه چون از سوئیچ زمینه سیستم آزاد شدهایم، میتوانیم نخهای مجازی بسیار بیشتری را به کار بگیریم.
یک مشخصه کلیدی نخهای مجازی این است که نخ حامل را مسدود نمیسازند. به این ترتیب مسدودسازی یک نخ مجازی به عملیاتی بسیار ارزانتر تبدیل میشود، چون HVM نخ مجازی دیگری را زمانبندی میکند و نخ حامل مسدود نمیشود.
API جدید Builder برای نخ
در Loom یک API جدید builder در کلاس Thread داریم که همراه با چند متد factory دیگر ارائه شده است. در این بخش به بررسی شیوه ایجاد کارخانههای استاندارد و مجازی و بهرهگیری از آنها برای اجرای نخ خود میپردازیم:
خروجی اجرای کد فوق به صورت زیر است:
مدخل نخست خروجی استاندارد toString مربوط به نخ کرنل است. اکنون در خروجی میبینیم که نخ مجازی هیچ نامی ندارد و روی نخ کارگر از استخر Fork-Join مربوط به گروه نخهای CarrierThreads اجرا میشود.
چنان که میبینیم صرفنظر از پیادهسازی زیرین، API همان است و معنی این گفته آن است که میتوانیم به سادگی کد موجود را روی نخهای مجازی اجرا کنیم. ضمناً نیازی به یادگیری API جدید برای بهرهگیری از این نخهای مجازی نداریم.
ترکیببندی نخ مجازی در جاوا
نخ مجازی از ترکیب یک continuation و یک scheduler ساخته میشود. این scheduler حالت کاربر میتواند هر پیادهسازی از اینترفیس Executor باشد. مثال فوق به ما نشان داد که به طور پیشفرض کد روی ForkJoinPool اجرا میشود.
به طور مشابه برای یک نخ کرنل که میتواند روی CPU اجرا شود و سپس پارک شده و مجدداً زمانبندی شود و سپس اجرای خود را تعلیق کند، یک continuation واحدی اجرایی است که میتواند آغاز شود و سپس پارک شود و مجدداً زمانبندی شده و اجرای خود را به ترتیبی تعلیق کند که کار را از جای باقیمانده از سر بگیرد و همه این موارد در حالی است که به جای سیستم عامل به وسیله JVM مدیریت میشود.
توجه کنید که continuation یک API سطح پایین است و برنامهنویسها باید از API-های سطح کرنل مانند builder API برای اجرای نخهای مجازی بهره بگیرند. با این حال برای این که نشان دهیم طرز کار آن در پسزمینه چگونه است یک continuation آزمایشی را اجرا میکنیم:
خروجی اجرای کد فوق به صورت زیر است:
در این مثال، continuation خود را اجرا میکنیم و در نقطهای از زمان تصمیم میگیریم که پردازش را متوقف کنیم. سپس در ادامه دوباره آن را اجرا میکنیم. بدین ترتیب continuation ما از جایی که کار را متوقف کرده بود از سر میگیرد. از روی خروجی متوجه میشویم که متد run() دو بار فراخوانی شده است، اما continuation یک بار آغاز شده و سپس اجرای خود را در اجرای دوم از جایی که باقی مانده بود، از سر گرفته است.
ما میخواهیم که عملیات مسدودساز از سوی JVM به این صورت اجرا شوند. زمانی که عملیات مسدودساز رخ میدهد، continuation تعلیق میشود و نخ حامل مسدود نمیشود. بنابراین اتفاقی که افتاده این است که نخ اصلی یک فریم پشته جدید روی پشته فراخوانی خود برای متد ()run ایجاد کرده و اجرا را تداوم بخشیده است. سپس بعد از آن که continuation تعلیق شد، JVM حالت کنونی اجرای آن را ذخیره کرده است.
در ادامه نخ اصلی اجرای خود را طوری ادامه داده که گویی متد ()run بازگشته است و با حلقه loop ادامه داده است. پس از فراخوانی دوم به متد ()run در continuation ،JVM حالت نخ اصلی را به نقطهای بازیابی کرده که continuation تعلیق شده بود و اجرا را خاتمه بخشیده است.
سخن پایانی
در این مقاله به بررسی تفاوت بین نخ کرنل و نخ مجازی پرداختیم. سپس شیوه استفاده از API بیلدر جدید از پروژه Loom برای اجرای نخ مجازی را نشان دادیم. در نهایت نشان دادیم که یک continuation چیست و چگونه کار میکند. همچنین به بررسی حالت پروژه Loom پرداختیم.