با تشکر از حجت جعفری برای ترجمه کله asynchronous
از آن جایی که به عقیده من دانستن مطالب پایه ای و احتمالا خواندن/مطالعه برنامه ها و روش های طراحی دیگران قویترین ابزارهای پیشرفت ما برنامه نویس ها هستند، امروز می خواهم در باره کد ناهمگام و اهمیت آن صحبت کنم. کد ناهمگام امروزه دیگر یک مفهوم خاص برای جاهای خاص نیست و در بسیاری از پلتفرم های برنامه نویسی رخنه کرده است (اگر معادل فارسی خوب و قابل فهم غیر عجیب برای platform می دانید، لطفا در کامنت ها به من بگویید. اخطار کسی که سکو را پیشنهاد دهد در طبقه دوازدهم جهنم در کنار javascript و visual basic با نسخه ابتدایی DOS همنشین و ... خواهد شد). تقریبا همه توابع در API های Universal Windows Platform در Windows 10 ناهمگام هستند و مشهور است که در Facebook تمام API های داخلی مورد استفاده برنامه نویس ها ناهمگام هستند.
اما کد ناهمگام چیست؟ برنامه ای که در آن شما پس از درخواستی برای انجام یک کار، منتظر تمام شدن آن نمانید و به کار خود تا جای ممکن ادامه دهید و پس از باز گشتن پاسخ ، پردازش درخواست خود را ادامه دهید، شما در حال استفاده از کد ناهمگام هستید. معمولا درخواست های کند مثل خواندن و نوشتن دستگاه های ورودی I/O چون دیسک و یا خواندن/نوشتن در شبکه و هر عمل کند دیگری که احتمالا به این سیستم های ورودی و خروجی مربوط است و/یا شامل پردازش های طولانی می شوند به شکل ناهمگام پیاده سازی می شوند.
در زبان های مختلف برای اجرای کد ناهمگام یا تعریف آن مکانیزم های مختلفی وجود دارد. مثلا در سی شارپ async await تقریبا راحتترین روش برای فهم کد ناهمگام و تعریف و اجرای آن است که به وسیله بسیاری زبان های دیگر نیز در سال های اخیر پیاده سازی شده است. در برخی زبان ها به شما یک آبجکت به نام future برگردانده می شود که شما می توانید با استفاده از آن چک کنید که آیا عملیات ناهمگام تمام شده است یا خیر و یا برنامه را تا تمام شدن آن بلاک کنید. بلاک کردن برنامه بدین معنا خواهد بود که thread جاری برنامه دیگر هیچ کدی را اجرا نمی کند تا عملیات ناهمگام مربوطه به پایان برسد. از این لحظه به بعد برنامه شما به شکل همگام منتظر پاسخ آن عملیات خواهد ماند. اگر دقیقا پس از فراخوانی کد ناهمگام منتظر جواب آن برنامه را بلاک کنید آن گاه گویی کد همگام اجرا کرده اید.
معمولا کد ناهمگام به دو روش push و Pull قابل پیاده سازی است. در روش pull برنامه اصلی باید در بازه های زمانی چک کند و ببیند که آیا عملیات گرفتن داده از شبکه یا نوشتن روی دیسک و ... تمام شده است یا خیر. این روش به صرفه نیست و دیگر طقریبا مورد استفاده قرار نمی گیرد. در روش push طرف دیگر عملیات ناهمگام (مثلا دیسک) به شما سیگنالی ارسال می کند و یا وقفه ای تولید می کند که نشان می دهد عملیات تمام شده و شما می توانید نتیجه را گرفته و کار مربوط به آن عملیات را ادامه دهید. توضیحات کامل چگونگی اتفاق افتادن push بین سخت افزار و driver و سیستم عامل بخشی از مطلب فعلی نیست و به آن نمی پردازم. در سیستم های ناهمگام نرم افزاری هم شما معمولا از سمت دیگر عملیات ناهمگام به سمت فراخوان اطلاع می دهید که جواب حاضر است. مثلا اگر یک سیستم جست و جوی داده نوشته باشید که درخواست جست و جو دریافت می کند و پس از تمام شدن به شکل ناهمگام پاسخ را پس می دهد، برای پیاده سازی آن باید یا یک تابع (function pointer / delegate / ...) بگیرید که پس از تمام شدن عملیات آن را فراخوانی کنید و یا یک رفرنس به یک future را به فراخوان برگردانید که پس از تمام شدن کارتان نتیجه را در آن قرار خواهید داد. خیلی از سیستم ها در engine های مدرن بازی سازی بدین شکل کار می کنند. در بسیاری سیستم ها درخواست های بررسی به physics engine ارسال شده و physics engine پس از یافتن جواب آن را در صفی قرار می دهد و یا به گونه ای دیگر به درخواست کننده برمی گرداند. به خصوص در معماری بازی های تحت شبکه و MMO این کار بسیار معمول است.
اما فایده این همه پیچیدگی اضافه در کد و استفاده از سیستم های ناهمگام چیست؟ فایده اصلی این است که پردازنده و برنامه شما منتظر و معطل باقی نمی ماند تا عملیاتی نسبتا کند تمام شود وی شما می توانید به کار خود ادامه دهید یا درخواست هایی به که عملیات فعلی ربطی ندارند را پاسخ دهید. بدین شکل سیستم های کامپیوتری بسیار بهینه می شوند و قدرت پاسخ گویی بسیار بالاتری هم پیدا می کنند. فرض کنید یک برنامه وب سرور پس از گرفتن هر درخواست کل برنامه را تا خواندن صفحه مورد نظر از دیسک متوقف می کرد. در این صورت هر وب سرور احتمالا در ثانیه زیر 100 درخواست را پاسخ می داد. البته خیلی از وب سرور های قدیمیتر از روش های مثل ایجاد thread برای هر کاربر استفاده می کنند و داخل thread تا دلتان بخواهد توابع همگام اجرا می کنند ولی در وب سرور های مدرن و تمام سیستم های تحت اینترنت و حتی در تمام API های جدید Windows از کد غیر ترتیبی استفاده می شود. تعداد کاربران امروزی سیستم ها و با کامپیوتر های فعلی بدون استفاده از برنامه های ناهمگام و برنامه های توضیع شده قابل پاسخ گویی نیستند. ما باید سعی کنیم تا جای ممکن کد های مختلف را روی هسته های مختلف پردازنده به پیش ببریم و هرگز منتظر عملیاتی نمانیم مگر مجبور باشیم و نتوانیم تا رسیدن پاسخ به کار دیگری ادامه دهیم.
سیستم بسیار جالبی برای نوشتن کد کاملا ناهمگام و موازی به یونیتی هم اضافه شده است که به C# job system معروف است و در نسخه های سال 2018 کم کم به برنامه اضافه خواهد شد. البته این سیستم ها به جز امکان اجرای ناهمگام بسیاری درخواست ها ، امکانات مهم دیگری مثل امکان اجرای کد gameplay روی چندین هسته و thread به شکل موازی و استفاده از حافظه unmanaged را هم به شما می دهد که از دلایل مهم اثر بسیار بالای آن روی performance برنامه هستند.
معایب سیستم های ناهمگام پیچیدگی بیشتر و نامناسب بودن بسیاری از ابزارهای برنامه نویسی ما برای کار با آن ها هستند. مثلا در .NET تا همین نسخه 2.1 تمام stack trace های مربوط به کد async بسیار ناخوانا بودند. به طور کلی به بهترین ابزار ها هم ، آنالیز کد های ناهمگام بسیار سختتر است زیرا شما نمی دانید در هر لحظه چه کد های دیگری دارند چه کاری انجام می دهند. اگر برنامه خطی باشد بیشتر این مشکلات وجود ندارند.
امیدوارم این پست به شما کمک کند که قدرت واقعی و مشکلات کد ناهمگام را بهتر درک کنید و بتوانید برای استفاده از آن در بنامه های مختلف تصمیمات درست بگیرید.