اختاپوس خسته

یادداشت‌هایی پیرامون کد، زندگی و دوستان

پروژهٔ AIT

پروژهٔ AIT بزرگ‌ترین کار متن‌بازی هست که پیاده‌سازی کردم. AIT مخفف Artificial Intelligence Toolkit هست. هدف این کتابخانه فراهم کردن یک پلتفرم توسعه برای پروژه‌های تحقیقاتی و صنعتی در زمینهٔ هوش مصنوعی هست. حقیقتش اهداف بزرگ‌تری هم داره. مثلاً پر کردن خلأ بین آکادمی و صنعت در زمینهٔ هوش مصنوعی یکی از کارهایی هست که مدت‌هاست آرزو دارم انجامش بدم. توی پست دیگه در مورد این خلأ صحبت خواهم کرد.

این پست به بررسی کتابخانهٔ AIT و کاربردهای فوق‌العاده جالبی که می‌تونه داشته باشه خواهد پرداخت. صفحهٔ اصلی پروژه رو توی همین سایت گذاشتم و رپوزیتوری گیت هم در گیت‌هاب در دسترس هست.

صورت مسأله

و آمّا AIT… این پروژه اول به صورت یک تکلیف درسی در دانشگاه نوشته شد و بعداً تکمیل‌ترش کردم و به‌صورت متن‌باز (با اجازه‌نامهٔ LGPL 2.1) منتشر کردم. در حال حاضر نسخهٔ 0.1.0 هست و طبیعتاً نمی‌تونه توی پروژه‌های دیگه استفاده بشه. تا آخر امسال اولین نسخهٔ پایدارش رو منتشر خواهم کرد. همون‌طور که گفتم، AIT یک پلتفرم توسعه برای پروژه‌های تحقیقاتی و صنعتی هست.

ترم بهار ۹۱-۹۲ توی دانشگاه واحد درسی سیستم‌های همروند رو گذروندم. منتهی موضوع درس ارائه شده، اصلاً سیستم‌های همروند نبود بلکه «مسائل ارضای محدودیت» بود. چون ثبت درس اختیاری جدید سخته، با یه اسم دیگه ارائه داده بودن.

پروژهٔ این درس پیاده‌سازی الگوریتم Asynchronous Backtracking و مشتقاتش بود. این الگوریتم توسط Yokoo و همکارانش سال 1992 برای حل مسائل ارضای محدودیت توزیع‌شده (Distributed constraint optimization) ارائه شده. بهینه‌سازی‌های متعددی هم تا به‌حال براش نوشتن. وظیفهٔ ما برای پروژهٔ پایانی درس، این بود که الگوریتم‌های ABT، Agile ABT و FC رو برای حل مسألهٔ هشت‌وزیر پیاده‌سازی کنیم.

بزرگ‌ترین مشکل این پیاده‌سازی اینه که ماهیت مسأله توزیع شده است. مثلاً برای حل مسأله‌ای که \(n\) تا متغیر رو می‌خواد مقداردهی کنه، باید به همون تعداد عامل (agent) اجرا بشه و تمام این عامل‌ها با همدیگه ارسال و دریافت داده خواهند داشت.

مشکلات

اولین و بزرگ‌ترین مشکلی که برام به‌وجود اومد، برقراری ارتباط بین ایجنت‌ها بود. باید طوری بین ایجنت‌ها ارتباط برقرار می‌کردم که شرایط زیر ارضا می‌شد:

  • هر ایجنت قابلیت ارسال و دریافت داده به‌صورت ناهمگام از تمام ایجنت‌ها داشته باشه
  • ارتباطات قابل اعتماد باشه
  • ترتیب حضور ایجنت‌ها (حتا مانیتور!) مهم نباشه

ایجنت مانیتور به ایجنتی گفته میشه که مسئول مقداردهی به هیچ متغیری نیست و صرفاً پایان مسأله رو تشخیص میده و بقیهٔ ایجنت‌ها رو خاموش می‌کنه.

مشکل دیگه‌ای که پیدا کرده بودم نحوهٔ پکت‌بندی و ارسال و دریافت داده‌ها بین برنامه‌ها بود. معمولاً وقتی همچین سناریوای پیش میاد یه برنامه‌نویس گزینه‌های متعددی رو در اختیار داره که ازشون استفاده کنه. اگر خیلی ول‌انگار باشه و یا جزو برنامه‌نویس‌های نسل جدید (نفخ‌افزارنویس‌ها) باشه از ابزارهایی مثل WCF، XML، SOAP و یا حتا RPC برای انتقال داده استفاده می‌کنه. حتا مورد داشتیم برای ارسال و دریافت اطلاعات توی شبکه بین دو برنامه اصرار داشته که HTTP Request بفرستیم و بگیریم! (استاد درس مهندسی نرم‌افزار ۲). حتا اگر برنامه‌نویس خیلی خیلی راحت‌طلب باشه (مثل توسعه‌دهنده‌های شرکت فناوری اطلاعات بورس تهران) از مزخرفاتی مثل WSDL و JSON هم می‌تونه استفاده کنه.

اما متأسفانه {از دید خودم} و خوشبختانه {از دید برنامه‌ها} من به سبک برنامه‌نویس‌های دههٔ هفتاد و با دید کاربردگرا و مقیاس‌گرا به قضیه نگاه می‌کنم. یعنی تک‌تک بایت‌ها و ثانیه‌های مصرف شده در برنامه برای من مهم هستن. استراتژی‌های اصلی توسعهٔ نرم‌افزار رو همیشه با همین دید انتخاب می‌کنم. و البته معمولاً تو دردسر می‌افتم. در همین راستا تصمیم گرفتم با یک پروتکل فوق‌سبک و دست‌ساز داده‌ها رو بفرستم. خوب مسلماً اولین و ساده‌ترین چیزی که به ذهن می‌رسه Serialization هست.

اما کار به این‌جا ختم نمی‌شه! سریال‌کردن دیتا با پروتکل‌های سبک RFC مشکلات پیاده‌سازی داره. مهم‌ترین این مشکلات برقراری ویژگی‌های چند سکویی هست. قبلاً گفتم که یکی از معیارهای اصلی من برای توسعهٔ نرم‌افزار، نوشتن کد قابل حمل هست. یعنی برنامه‌ای که می‌نویسم باید روی تمام معماری‌ها ( ARM , x86 , x86_64 ) و تمام سیستم‌عامل‌ها، مثل لینوکس، یونیکس، FreeBSD, OS2 و حتا ویندوز؛ قابل اجرا باشه. برآورده کردن این هدف کار ساده‌ای نیست و اساس اکوسیستم (بسیار پردسر) توسعهٔ نرم‌افزار من رو تشکیل میده. از بحث اصلی خارج نشیم! سریال کردن داده‌ها به سبک پروتکل‌های RFC با حفظ قابلیت حمل کار ساده‌ای نیست. چطور؟ شما موانع زیادی برای برقراری ویژگی قابلیت حمل دارید از جمله: bitness یا همون endianness و ABI . اولی مربوط به معماری میشه. دومی هم همین‌طور :) bitness یک معماری ترتیب بیت‌ها رو مشخص می‌کنه. کلاً دو نوع bitness داریم. سیستم یا Little Endian هست یا Big Endian . از بین سیستم‌هایی که می‌شناسیم و باهاشون سروکار داریم، ARM از نوع BE هست و بقیه از نوع LE.

فرض کنید شما یک عدد صحیح رو می‌خواهید بین دو تا پروسس منتقل کنید. یکی از این پروسس‌ها روی سیستم‌عامل اندرویید گوشی‌تون اجرا میشه و دیگری روی لینوکس کامپیوترتون. اگر اولی یک عدد صحیح (int) رو به شکل 0A0B0C0D در چهار بایت ذخیره کرده باشه، دومی همون عدد رو به شکل 0D0C0B0A ذخیره می‌کنه! اصلاً کلمهٔ Little Endian هم از همین‌جا منشأ گرفته. یعنی بایت کم‌اهمیت، قبل از بایت پراهمیت ذخیره میشه.

نیازی به توضیح بیشتر نیست که اگر یک داده رو از سیستم اولی به سیستم دومی بفرستیم (به هر طریقی) شاهد یک فاجعه خواهیم بود. مگر این که مکانیزم فراوری داده رو تو پایین‌ترین سطح مدیریت کنیم! طبیعتاً توسعه‌دهنده‌های حرفه‌ای قبل از ما راهکار مناسب رو ارائه دادن. با یک جستجوی ساده توی اینترنت و خوندن چند تا راهنما و مقایسه به این نتیجه می‌رسیم که ابزارهایی مثل Boost Serialization Library و چند تای دیگه حذف میشن. در آخر می‌مونه کتابخانهٔ بسیار عالی و پرمحتوای Google Protocols Buffers . بافرهای پروتکل کتابخونه‌ای هست که به گفتهٔ خود نویسندگانش در گوگل به‌شکل درون‌سازمانی استفاده میشه و همون‌طور که از اسمش پیداست برای پیاده‌سازی پروتکل‌ها (احتمالاً RFC ها و غیره) ازش استفاده می‌کنن. تمام مسائل و مشکلات مربوط به معماری، اعم از bitness، تفاوت PDTها در زبان‌ها و همچنین نمایش‌های باینری متفاوت در پلتفرم‌های مختلف، در نظر گرفته شده و حل شده :)

وقتی برنامه‌ها با هم حرف می‌زنند

با استفاده از PB مشکلات مربوط به ساخت پکت‌های باینری حل میشه. اما چطوری این پکت‌ها رو بین برنامه‌ها رد و بدل می‌کنیم؟ خوب جواب اول استفاده از مکانیزم‌های معروف IPC (همون ارتباط میان‌پردازه‌ای) مثل Shared Memory و یا pipe هست. اما من ترجیح میدم از سوکت‌ها استفاده کنم. چون میشه بعداً گسترده‌ترش کرد و روی چند تا کامپیوتر اجراش کرد. با این حال API های استاندارد سوکت‌ها خیلی سطح پایین هستند و کار کردن باهاشون برای کسی که نمی‌خواد وارد دردسرهای سناریوهای مختلف ارتباطی بشه، کار چندان جالبی نیست. منظورم از سناریو، حالت‌های متداولی هست که ممکنه پیش بیاد. بذارید چند تا مثال بزنم:

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

خوب این‌جا هم یه پیاده‌سازی خیلی عالی وجود داره به اسم ∅MQ یا همون ZeroMQ . این کتابخانه در واقع پیاده‌سازی یک لایهٔ میانی بین Transport Layer و Application Layer در مدل شبکه هست. از این ابزار به عنوان Intelligent Transport Layer هم یاد می‌کنند.

گزینهٔ دیگه‌ای هم برای برقراری ارتباط وجود داشت و اون هم استفاده از OpenMPI بود که کار ∅MQ و Protocol Buffers رو یک‌جا انجام میده. اما از ازش استفاده نکردم تنها به یک دلیل: در آینده می‌خوام این برنامه روی اینترنت و شبکه‌های پیچیده تست بشه. بنابراین باید ویژگی NAT Traversal رو براش پیاده‌سازی کنم که با استفاده از API ی OpenMPI قابل پیاده‌سازی نیست.

دیدگاه‌ها