در واقع اکتور شبیه نقش انسان است در یک سیستم. تعامل اکتور ها درست مانند انسان ها است با رد و بدل کردن پیام. اکتور ها هم مانند انسان ها میتوانند در بین پیام هایی که تبادل میکنند کارهایی انجام دهند.
تصور کنید چیزی شبیه یک مرکز تماس و پشتیبانی که صدها مشتری به صورت همزمان میتوانند با آن تماس بگیرند و مکالمه خاص خود با یکی از پاسخگوهای مرکز را داشته باشند. این مدل از تعامل بین افراد می تواند به صورت زیر با اکتور ها مدل شود:
در اکتور مدل هر چیزی یک اکتور است. درست مانند برنامه نویسی شی گرا که در آن هر چیزی یک شی محسوب میشود.
در C# مساله را به صورت کلاس ها و آبجکت ها در دومین خاص تعریف میکنیم و در akka.net مسائل به صورت اکتور و پیام تعریف میشود.
یک مثال ساده از untypedActor در akka.net به صورت زیر است:
انواع مختلفی از اکتور در akka.net وجود دارد ولی در نهایت همه آنها از UntypedActor مشتق شده اند.
احتمالا متوجه متد OnRecieve از کد بالا در BasicActor شده اید:
اینجا قسمتی از اکتور است که پیام ها را دریافت میکند. در akka پیام یک آبجکت است. پیام میتواند یک رشته حروف یا یک عدد باشد یا یک کلاس که شما ساخته اید و تعریف کرده اید.
مانند: OfficeStaff.RequestMoreCoffe
اکتورها معمولا برای کار کردن با تعداد محدود و تعریف شده ای از انواع پیام ها طراحی می شوند اما اگر پیامی با تایپ تعریف نشده ای برای یک اکتور ارسال شود اتفاق بدی نمی افتد! فقط یک لاگ unhandled درج میشود و کار ادامه پیدا میکند.
پیام ها کاملا تغییر ناپذیر هستند. خوب این به چه معنی است؟ یک شی تغییر ناپذیر چگونه شی است ؟
جواب اینکه یک شی تغییر ناپذیر شی است که وضعیت آن مثلا محتوای درون حافظه آن بعد از اینکه ساخته شد تغییر نمیکند. نباید تغییر کند! نمی تواند تغییر کند.
مثلا در دات نت کلاس string یک شی غیر قابل تغییر است.
اتفاقی که می افتد به این صورت است:
چون string اصلی تغییر ناپذیر است ، تمام عملیات ها با ساختن کپی های جدید که تغییرات گرفته انجام میشود.
در akka پیام های تغییر ناپذیر به صورت ذاتی thread-safe هستند. یعنی هیچ تردی نمی تواند محتوای پیام را تغییر دهد بنابراین ترد دومی که پیام را دریافت میکند نگران این نیست که ترد قبلی تغییری در محتوای پیام ایجاد کرده است.
بنابراین در akka تمام پیام هایی که بین اکتور ها رد وبدل میشود immutable و thread-safe هستند و به همین دلیل است که در akka هزاران اکتور به صورت همزمان می توانند پیام های همزمان را پردازش کنند بدون اینکه نگران مکانیزم هایی مانند سینکرونازیشن باشند.
به صورت اجمالی با اجزای پیام ها و اکتور ها آشنا شدیم. حالا زمان آن است که ببینیم اینها چگونه باهم کار میکنند.
در برنامه نویسی شی گرا ، اشیا از طریق فراخوانی فانکشن های همدیگر عمل میکنند. در برنامه نویسی رویه ای هم به همین صورت است. کلاس A یک متد از کلاس B را فراخوانی میکند و منتظر میماند تا متد جواب بدهد سپس کلاس A می تواند کارش را ادامه دهد.
در akka و اکتور مدل با ارسال پیام به یکدیگر ارتباط اکتور ها صورت می پذیرد. چه نکته ای در این روش و ایده حائز اهمیت است؟
اینکه ارسال پیام ها به صورت Asynchronous انجام میشود به این معنی که اکتور فرستنده پس از ارسال پیام به اکتور گیرنده میتواند بدون آنکه منتظر جواب آن باشد به کار خود ادامه دهد در این زمان اکتور گیرنده به پردازش پیام های دریافتی مشغول میشود در نتیجه تمام تعاملات بین اکتور ها به صورت Async انجام میشود.
اما یک مزیت مهم و جالب دیگر هم وجود دارد! از آنجا که تمام عملیات از طریق پیام ها صورت میگیرد و هر پیام یک آبجکت مجزا و منحصر به فرد است ، اکتور میتواند لیست و تاریخچه ای از آن ها را داشته باشد و حتی می تواند پردازش بعضی از آنها به تعویق اندازد!
تصور کنید که بخواهید عملیات undo را با اکتور پیاده سازی کنید. هر تغییری به صورت یک پیام است. برای undo کردن یکی از این تغییرات کافی است آن را از لیست پیام های اکتور پیدا کنید و آنرا به اکتور یکه مسئول تغییر وضعیت است بفرستید! به همین سادگی.
یک قابلیت فوق العاده دیگر هم در اکتور سیستم وجود دارد: Location Transparency
لوکیشن ترنسپرنسی به چه معنی است ؟ یعنی در اکتور سیستم هنگامی که شما قصد دارید پیامی را به اکتوری بفرستید ، نیازی نیست تا بدانید آن آکتور کجای سیستم قرار گرفته است. سیستمی که در آن ممکن است شامل صد ها ماشین باشد! فقط کافی است تا آدرس آن اکتور را بدانید!
برای مثال اگر شما بخواهید به دوستتان تلفن بزنید لازم نیست محل واقعی او را در کشور و شهر و کوچه و خیابان ها بدانید! کافی است شماره تلفن او را داشته باشید تا بتوانید با او تماس بگیرید چون شرکت مخابرات بقیه کار را برای شما انجام میدهد.
اکتور ها هم به همین نحو عمل میکنند. هر اکتوری یک آدرس با ساختاری خاص دارد که آن را در سیستم دسترس پذیر میکند.
مانند اینترنت که http, https, ftp و … دارد، در akka برای ارتباط بین پروسه ها میتوان از پروتکل های مختلفی استفاده کرد. پیشفرض برای اکتورسیستمی که به صورت تک پروسسی است از akka:// استفاده میشود. اگر از remoting یا clustering بخواهیم استفاده کنیم akka.tcp:// یا akka.udp:// قابل استفاده است تا بتوان بین نود ها ارتباط برقرار کرد.
هر اکتور سیستمی لازم است در ابتدای شروع و ایجاد یک نام داشته باشد که بین تمام پروسس ها یا ماشین هایی که در سیستم توزیع شده akka وجود دارد مشترک است.
اگر از remorting استفاده نکنیم این قسمت از ActorPath قابل حذف شدن است در غیر این صورت به صورت IP یا Domain name استفاده میشود تا ارتباط remote انجام پذیر باشد
این مسیر یک اکتور مشخص است . ساختار آن درست شبیه URL است با این نکته که تمام اکتور هایی که توسط کاربر ساخته میشود در شاخه ی /user/ از اکتور اصلی (root actor) قرار میگیرند.
بنابراین وقتی میخواهید یک پیام برای اکتوری بفرستید، پیام را به آدرس آن ارسال می کنید:
ارسال پیام به یک ریموت اکتور درست مانند ارسال پیام به یک لوکال اکتور است! این همان لوکیشن ترنسپرنسی است که بالاتر به آن اشاره کردیم.
هنگامی که پیامی به اکتور ارسال میشود به صورت مستقیم به متد OnReceive نمی رود.
پیام درون mailbox آن اکتور قرار میگیرد که در آن ترتیب ورود و خروج رعایت میشود. یعنی پیامی که زودتر آمده زودتر نیز پردازش میشود. درست مانند یک صف. میل باکس کار ساده ای انجام میدهد. پیام را میگیرد و نگه میدارد تا اکتور آماده پردازش کردن آن بشود. وقتی که اکتور آماده بود میل باکس پیام را به متد OnRecieve میرساند و عملیات پردازش پیام در اکتور آغاز میشود.
در akka.net اینکه actor context و internal state همیشه به صورت thread-safe هنگام پردازش پیام ها عمل کنند تضمین شده است.
دلایلی که برای این موضوع وجود دارد از این قرار است:
بنابراین یک اکتور نمیتواند پیام دوم را پردازش کند مادامی که از متد OnRecieve خارج نشده باشد. هنگامی که از این متد خارج شود mailbox پیام بعدی را به OnRecieve میفرستد.
درست مثل کلاس های دیگری که در C# وجود دارد، هر اکتور میتواند خواص و فیلد های خود را داشته باشد.
وقتی اکتور ری استارت میشود ، اینستنس آن از بین میرود و مجددا ساخته میشود.
نمونه جدید ساخته شده با آرگومان های مربوط به کانستراکتور از طریق چیزی به نام Props به نمونه جدید داده میشود. در مورد Props صحبت خواهیم کرد اما فعلا به آن به چشم یک طرز تهیه اکتور (مانند دستور پخت) نگاه کنید.
دلیلی که به این موضوع اشاره کردیم این است که میخواهیم در مورد "چرخه زندگی" اکتور صحبت کنیم و باید به یاد داشته باشیم که که akka.net هر زمانی که بخواهیم یا هر زمانی که مشکلی پیش بیاید میتواند یک اکتور را از نو راه اندازی کند و اکتور را به وضعیت ابتدایی آن منتقل کند.
قبل از اینکه یک اکتور آماده شروع به پردازش پیام های درون صندوقش باشد ، باید توسط "Actor System" نمونه سازی شود یا ساده تر اینکه ایجاد و آماده شود.
اکتور ها ابتدا ساخته می شوند و سپس شروع به کار میکنند و بیشتر عمرشان را صرف پردازش پیام های دریافتی میکنند و هنگامی که دیگر نیازی به آن ها نیست می توانید آن را متوقف یا به عمرش پایان دهید.
هنگامی که یک اکتور به طور تصادفی دچار کرش می شود مثلا در مواقعی که یک unhandled exception رخ میدهد ناظر آن اکتور آن را ری استارت میکند و چرخه زندگی آن از ابتدا شروع میشود. نکته جالب و مهم این است که هیچ کدام از پیام های باقی مانده و پردازش نشده در mailbox آن اکتور از بین نمی رود و منتظر میماند تا اکتور آماده ی پردازش گردد.
1- Actor’s constructor
مانند هر کلاس دیگر در C# میتوانید آرگومان های مورد نظر را به سازنده ی کلاس بدهید
2- PreStart
در این قسمت می توانید کدی را قرار دهید که باید قبل از شروع دریافت پیام ها اجرا شود و محل خوبی است برای قرار دادن منطق و مقداردهی های اولیه. این قسمت هنگام ری استارت هم فراخوانی میشود.
3- PreRestart
اگر اکتور به طور اتفاقی دچار مشکل شود والد اکتور آن را ری استارت میکند
4- PostStop
یکبار هنگامی که اکتور متوقف میشود فراخوانده میشود. بعد از آن اکتور هیچ پیامی دریافت نمیکند. اینجا جایی است که میتوان در آن به پاکسازی اقدام کرد. این متد هنگام ری استارت شدن اکتور فراخوانی نمیشود و فقط هنگامی که عمدا اکتور را از بین میبرید فراخوانده میشود.
5- PostRestart
هنگام ری استارت شدن بعد از PreRestart و قبل از PreStart فراخوانی میشود. اینجا جایی است که میتوانید دیاگ ها لازم یا لاگ هایی در مورد کرش اکتور را ثبت کنید.
درست مانند انسان ها ، هر اکتوری حتما والد دارد و ممکن است خواهر و برادر یا فرزند داشته باشند ولی لزوما فرزند ندارد.
این بدین معنی است که هر اکتوری باید توسط یک اکتور دیگر ایجاد شود.
پس وقتی به صورت زیر عمل میکنیم:
در واقع ما یک اکتور فرزند برای اکتور اصلی /user/ ایجاد میکنیم در نتیجه مسیر این اکتور جدید به این صورت است:
/user/myActor/
به همین صورت میتوانیم از داخل یک اکتور ، یک اکتور دیگر ایجاد کنیم:
در این مورد مسیر اکتور جدید به این صورت است:
/user/myActor/child1/
ولی چرا این موضوع مهم است؟
قبل تر در قسمت چرخه حیات اکتور با این مفهوم آشنا شدیم که اکتور ها توسط ناظرشان ری استارت میشوند. هر اکتور والدی پیام مخصوص مربوط به کرش کردن فرزندش را دریافت میکند.
اکتور های والد دارای استراتژی نظارت هستند(میتوانید خودتان هم یک استراتژی تعریف کنید) که به آن ها این امکان را میدهد که به چه صورت کرش و خرابی اکتور فرزند را مدیریت کنند. اکتور های والد می توانند یکی از تصمیمات زیر را اجرا کنند:
1- Restart
راه اندازی مجدد اکتور خراب شده که به صورت پیش فرض اکتور والد به صورت متوالی در بازه ی زمانی کوتاهی انجام میدهد.
2- Stop
متوقف کردن اکتور خراب که به صورت دائمی آن را انجام میدهد.
3- Escalate
در این حالت خرابی به ناظر های بالاتر نیز منتقل میشود
وقتی که Stop یا Restart اتفاق بیفتد ، تمام فرزندان اکتور والد تحت تاثیر واقع می شوند و یا همگی کشته میشوند یا همه مجدد راه اندازی میشوند.
در واقع می توانید کل قسمتی از یک شاخه یا درخت اکتور ها را که از کار افتاده است را راه اندازی مجدد کنید.
این یک مفهوم واقعا قدرتمند برای قابلیت اطمینان(Reliability) و خود درمانی(self-healing) است که در آینده با جزئیات بیشتری به آن خواهیم پرداخت.
بعد از تمام این ها ، اگر هر اکتور در لحظه تنها یک پیام را میتواند پردازش کند ، آیا این به شدت کند نیست ؟
این سوال خوبی است پس ببینیم چگونه مجموعه اکتور ها در اکتور سیستم یک سوپر ماشین پردازشی ایجاد میکند.
بر اساس بنچمارکی که در سایت Akka.net وجود دارد، شما میتوانید 2.5 میلیون اکتور را در تنها 1 گیگابایت رم جا دهید. ضمنا با بهینه سازی های جدیدی که روی این کتابخانه ایجاد شده میتوان بهتر از این هم عمل کرد. نکته اصلی این است که اکتور ها خیلی سبک و کم هزینه هستند و میتوانید به وفور و بدون نگرانی از آن ها استفاده کنید.
اکتور ها کم هزینه هستند و میتوان به راحتی هزاران اکتور ایجاد کرد. یک اکتور میتواند یک پیام را در لحظه پردازش کند ولی هزاران اکتور هزاران پیام را در لحظه پردازش میکنند. با این تکنیک است که می توان پتانسیل پردازش همزمان اکتور سیستم را آزاد کرد و از آن بهره برد! با مقیاس عظیمی از اکتور های در حال پردازش.
در واقع شما بیشترین مزیت اکتور ها را موقعی به دست می آورید که از تعداد زیادی از آن ها استفاده کنید. اگر کل برنامه شما با یک اکتور نوشته شود قطعا به طرز وحشتناکی کند خواهد بود!
بنابراین برنامه ی شما و اکتور سیستم باید به گونه ای طراحی شود که با تعداد زیادی اکتور پردازش ها صورت پذیرد تا بتوانید از قابلیت های واقعی پردازش همزمان اکتور مدل بهره مند شوید.
حالا اگر یک اکتور پیامی برای پردازش نداشته باشد چه اتفاقی خواهد افتاد ؟
اگر پیامی نباشد اکتور هم هیچ کاری انجام نمیدهد. فقط اکتور هایی که صندوقشان حاوی پیام است کار میکنند و از پردازنده ی شما کار میکشند. اکتوری که پیامی در صندوق ندارد هیچ ترد یا منابع دیگری از سرور را اشغال نمیکند. اکتور ها "ری اکتیو" هستند. آن ها منتظر میمانند تا پیامی برسد تا از خواب بیدار شوند. این یکی از دلایل سبک و کم هزینه بودن اکتور ها است.