اکتور مدل چندین سال پیش توسط Carl Hewitt ارائه شد تا بتوان پردازش همزان درخواست در یک شبکه با کارایی بالا انجام داد. امروزه به سختی میتوان کارایی سختافزارهای موجود را به طور چشمگیری افزایش داد. برای همین، شرکتها به سیستمهای توزیع شده رو آوردهاند. اما این سیستمها با مدل رایج برنامهنویسی شیگرا (OOP) چالشهای زیادی دارند.
امروزه اکتور مدل یک راه کار کارا برای این مشکل به حساب میآید و در بسیاری از محصولات توانسته بار زیادی را تحمل کند.
هسته اصلی OOP همان encapsulation است. encapsulation به این معناست که دادههای داخلی هر شی نباید به صورت مستقیم از بیرون قابل دسترس باشد و تنها باید بتوان آن را با استفاده از صدا زدن متدها تغییر داد.
میتوان رفتار OOP موقع اجرا را به صورت دنبالهای از پیامها نشان داد. مانند شکل زیر:
اما شکل بالا به طور کامل رفتار سیستم را نشان نمیدهد. در واقعیت ترد (thread) اجرا متدها را بر عهده دارد. اگر شکل بالا را بخواهیم تصحیح کنیم به صورت زیر خواهد شد:
حال فرض کنید بهجای یک ترد چند ترد داشته باشیم. اینجاست که مشکل سیستم مشخص میشود.
همانطور که دیده میشود، بخشی از اجرا وجود دارد که دو ترد وارد یک متد یکسان شدهاند. متاسفانه encapsulation در اینجا هیچ چیزی در مورد اینکه چه اتفاقی رخ خواهد داد، تضمین نمیکند. اجرای دو تابع میتوان به طور نامشخصی درهم آمیخته شود. این موضوع تنها با مدیریت دو ترد حل خواهد شد. حال فرض کنید در یک سیستم به جای دو ترد، چندین ترد وجود داشته باشد!!
راه حل متداول برای حل این مشکل استفاده از lock پیرامون این متدهاست. با وجود اینکه، این کار تضمین میکند که در هر لحظه تنها یک ترد، در حال اجرای متد است، اما مشکلاتی به وجود میآورد:
۱. هم زمانی (concurrency) به شدت محدود میشود.در CPUهای مدرن تغییر بین تردها بسیار سنگین و هزینهبر است.
۲. تردی که متد را صدا زده است، کاری نمیتواند انجام دهد و باید منتظر اتمام متد باشد.
۳. از طرفی lock باعث میشود یک خطر جدید به وجود آید. deadlock!
اکنون در وضعیتی قرار داریم که اگر lock کم داشته باشیم، ممکن است دیتا خراب شود. از طرفی اگر lock زیاد داشته باشیم کارایی کم میشود و ممکن است با deadlock مواجه شویم.
همچنین lock کردن تنها به صورت محلی به خوبی کار میکند. وقتی برنامه قرار است روی چندین ماشین اجرا شود، تنها میتوان از distributed locks استفاده کرد. اما lockهای توزیع شده چندین برابر ناکارآمدتر از lock به صورت محلی هستند و محدودیتهایی هم به رشد سیستم اضافه میکنند.
مدلهای برنامه نویسی مربوط به دهه ۸۰ و ۹۰ معمولا این فرض را میکنند که نوشتن در یک متغیر به معنای نوشتن به صورت مستقیم در حافظه است. اما در معماری امروزی، CPUها به جای نوشتن در حافظه، اطلاعات را در کش ذخیره میکنند. بیشتر این کشها برای هر هسته جدا هستند. یعنی نوشتن بر روی یکی توسط هسته دیگر قابل دیدن نیست. برای اینکه اطلاعات کش، برای هسته دیگر قابل دیدن شود، باید اطلاعات به کش هسته دیگر منتقل شود. این موضوع باعث میشود جابهجایی بین تردهای مختلف هزینهبر شود و باعث کندی سیستم شود.
اکتور مدل به ما این اجازه را میدهد که برنامه را به صورت ارتباط اشیا با یکدیگر ببنیم.
در اکتور مدل میتوان:
در اکتور مدل، اکتورها به جای صدا زدن تابع، به یک دیگر پیام میدهند. ارسال پیام، اجرای یک ترد را از شی فرستنده به شی گیرنده منتقل نمیکند و یک اکتور میتواند بعد از ارسال پیام، بدون بلاک شدن به کار خود ادامه دهد. اکتورها نیز با دریافت یک پیام، آن را پردازش کرده و سراغ پیام بعدی میروند.
یک تفاوت مهم ارسال پیام با صدا زدن تابع این است که پیامها مقداری را بر نمیگردانند. وقتی یک اکتور پیامی را به اکتور دیگری میفرستد، دیگر نیاز به منتظر بودن برای جواب نخواهد بود.
تفاوت مهم دیگر، نحوه تثبیت کردن، حالت درونی شی است. در اکتور مدل، وقتی چند ترد متفاوت پیامی به اکتور میفرستند، اکتور پیامها را در یک صفی قرار میدهد و آنها را یکی یکی پردازش میکند. در این حالت دیگر نیازی به نگرانی از بابت تغییر حالت شی، توسط چند ترد به صورت همزمان نیست. البته توجه داشته باشید که با وجود اینکه هر اکتور پیامهای خود را به صورت متوالی پردازش میکند، اکتورهای مختلف به صورت موازی کار میکنند. در نتیجه در سیستم، میتوان با توجه به سختافزار موجود، میلیونها پیام را به صورت همزمان پردازش کرد.
به طور خلاصه در این سیستم هر اکتور دارای:
در زبان go با استفاده از protoactor-go که یک پیاده سازی متن باز از اکتور مدل است، میتوانید به راحتی برنامههای مقیاس پذیر درست کنید.
مثل هیشمه میخواهیم برنامهای بنویسیم که عبارت Hello World را چاپ کند. برای اینکار یک اکتور تعریف میکنیم که هنگامی که پیامی از جنس Hello دریافت کرد، این عبارت را چاپ کند.
type Hello struct{ Who string } type HelloActor struct{} func (state *HelloActor) Receive(context actor.Context) { switch msg := context.Message().(type) { case Hello: fmt.Printf("Hello %v\n", msg.Who) } } func main() { context := actor.EmptyRootContext props := actor.PropsFromProducer(func() actor.Actor { return &HelloActor{} }) pid, err := context.Spawn(props) if err != nil { panic(err) } context.Send(pid, Hello{Who: "World"} console.ReadLine() context.Send(pid, Hello{Who: "World"})console.ReadLine() console.ReadLine() }
در کد بالا یک کلاس از نوع نوع Hello داریم. همچنین یک اکتور HelloWorld تعریف کردهایم.
برای هر اکتور باید تابع Recieve را اضافه کنیم که رفتار اکتور را در برابر هر پیام مشخص میکند. در این تابع بر اساس نوع پیامی که فرستاده شده است عملکرد اکتور را مشخص میکنیم. اگر پیام از نوع Hello بود، عبارت مشخص شده نمایش داده میشود.
برای اجرای یک اکتور ابتدا باید یک Context بسازیم. این کلاس، مدیریت اکتورها و تبادل پیامها را بر عهده دارد. برای ساخت هر اکتور یک props باید آماده کنیم که به نوعی نحوه ساخت یک اکتور را مشخص میکند. در اینجا فقط باید یک شی از HelloActor را برگرداند.
حال در context خود یک اکتور ساخته و id آن که همان آدرس اکتور است را ذخیره میکنیم. با استفاده از تابع Send میتوانیم به هر آدرسی که خواستیم، یک پیام ارسال کنیم.