همروندی با پارادایم CSP

توی پست قبل دربارهٔ برنامه‌نویسی تابعی به عنوان یک راه برای اجتناب از داده‌های مشترک و mutable صحبت کردیم. توی این پست می‌خوام دربارهٔ یک پارادایم دیگه صحبت کنم: Communicating Sequential Processes یا CSP

همروندی با پارادایم CSP

توی پست قبل دربارهٔ برنامه‌نویسی تابعی به عنوان یک راه برای اجتناب از داده‌های مشترک و mutable صحبت کردیم. توی این پست می‌خوام دربارهٔ یک پارادایم دیگه صحبت کنم: Communicating Sequential Processes یا CSP

در CSP تقریبا هیچ دادهٔ‌ مشترکی بین تردها وجود نداره و تردها کاملا از هم جدا هستند و از طریق فرستادن پیام با همدیگه ارتباط برقرار می‌کنند. درواقع تنها دادهٔ مشترکی که وجود داره، کانال‌های ارتباطی‌ای هستند که پیام‌ها از طریق اونها بین تردها جابجا می‌شوند. زبان ارلنگ (Erlang) از این پارادایم استفاده می‌کنه.

درواقع CSP اینجوریه که تردها کاملا از همدیگه جدا هستند(البته در سی++ نمیشه گفت کاملا؛ چون تردها از یک فضای آدرس استفاده می‌کنند. پس باید خودمون به شکل انتزاعی جدا بدونیمشون) و فقط و فقط از طریق کانال‌های ارتباطی با همدیگه پیام رد و بدل می‌کنند. یکجورایی کارشون پاسخ دادن به پیام‌های دریافتیه. بهترین چیزی که باهاش می‌تونیم همچین پارادایمی رو نشون بدیم، یک ماشین حالت یا State Machine هست. درواقع یک راه پیاده‌سازی این پارادایم،‌ پیاده سازی یک ماشین حالت متناهی هست(البته راه‌های دیگه هم وجود داره که خب من بلد نیستم).

توی این ماشین حالت، هر ترد پیام مربوط به خودش رو دریافت می‌کنه، وضعیت خودش رو به‌روزرسانی می‌کنه و احتمالا کنترل ماشین رو میده دست یک ترد دیگه.

کتاب، برای مثال یک نرم‌افزار خیلی سادهٔ دستگاه ATM رو مثال زده که البته توی این قسمت بازهم ساده‌ترش کرده و کد پیچیده‌ترش رو در قسمت ضمایم قرار داده. بهرحال، برای یک دستگاه ATM سه ترد مجزا در نظر گرفته.

  • یک ترد برای قسمت‌های فیزیکی دستگاه مثل دکمه‌ها، دریافت‌کننده کارت، نمایش متن و ...
  • یک ترد برای ارتباط با بانک
  • یک ترد هم برای انجام  کارهای مرتبط با منطق ATM

باید یک State Machine تشکیل بدیم و اون رو پیاده سازی بکنیم. ماشین حالت احتمالا چیزی به این شکل خواهد بود:

حالا طبق این طرح می‌تونیم کد خودمون رو پیاده‌سازی بکنیم. می‌تونیم یک کلاس داشته باشیم که هر Member-Function اون یکی از state های این ماشین باشند. درواقع هر حالت منتظر یک‌سری پیام‌های خاص می‌مونه، کارهای مرتبط بهش رو انجام میده و در صورت لزوم، state بعدی که قراره ماشین روی اون سوئیچ بکنه رو مشخص می‌کنه.

کد فعلی‌ش چیزی شبیه به این میشه:

void atm::getting_pin()
{
    incoming.wait()
    .handle<digit_pressed>(
    	[&](digit_pressed const& msg)
        {
            unsigned const pin_length=4;
            pin+=msg.digit;
            if(pin.length()==pin_length)
            {
                bank.send(verify_pin(account,pin,incoming));
                state=&atm::verifying_pin;
            }
        }
    )
    .handle<clear_last_pressed>(
    [&](clear_last_pressed const& msg)
        {
            if(!pin.empty())
            {
                pin.resize(pin.length()-1);
            }
        }
    )
    .handle<cancel_pressed>(
    [&](cancel_pressed const& msg)
    {
        state=&atm::done_processing;
    });
);

درواقع تمام جزئیات مربوط به Synchronization رد و بدل کردن پیام دیگه ارتباطی با ما نداره و مسئولیتش با کتابخانه‌ی Messaging هست. بنابراین تنها چیزی که برای ما مهم هست اینه که چه پیامی باید دریافت بشه و چه پیامی باید ارسال بشه.

توی این کد، ماشین حالت(درواقع بخشی که ماشین حالت رو داره کنترل می‌کنه) توی یک ترد و بخش‌های مربوط به atm مثل رابط ترمینال و رابط با بانک توی تردهای مختص به خودشون اجرا میشن. به این استایل برنامه‌نویسی، Actor Model هم گفته میشه. طبق این مدل، ما چند Actor جدا از هم داریم(که روی تردهای مجزا اجرا میشن) که به همدیگه پیام ارسال می‌کنن تا وظیفه مورد نظر انجام بشه.

به نظرم کد نیاز به توضیح نداره(حداقل الآن که تازه این بخش رو خوندم این حس رو دارم :))) ) بنابراین پیش میریم و پیاده‌سازی تابع getting_ping رو هم یک نگاه می‌ندازیم که کمی تا قسمتی پیچیده‌تر از کد قبل هست:

void atm::getting_pin()
{
    incoming.wait()
    .handle<digit_pressed>(
    	[&](digit_pressed const& msg)
        {
            unsigned const pin_length=4;
            pin+=msg.digit;
            if(pin.length()==pin_length)
            {
                bank.send(verify_pin(account,pin,incoming));
                state=&atm::verifying_pin;
            }
        }
    )
    .handle<clear_last_pressed>(
    [&](clear_last_pressed const& msg)
        {
            if(!pin.empty())
            {
                pin.resize(pin.length()-1);
            }
        }
    )
    .handle<cancel_pressed>(
    [&](cancel_pressed const& msg)
    {
        state=&atm::done_processing;
    });
);

تنها فرق این کد با کد قبلی اینه که لزوما توی هر دریافت پیام، state عوض نشده.

پایان

همونطور که دیدیم، این نوع برنامه‌نویسی میتونه خیلی از پیچیدگی‌های مربوط به اشتراک داده‌ها رو از بین ببره. مثالی هم که زده شد به نظر خودم و کتاب، یک مثال خوب برای Separation of Concerns هست چراکه باید کارها رو به خوبی از همدیگه جدا کنیم.

پست بدی نبود؛ تا پست بعدی، خدانگهدار.