درس 35: Actors and Processes
اکتورها و پراسس ها روش های جالبی رو برای پیاده سازی concurrency بهمون ارائه میدن، بدون بلایایی که shared memory ها سرمون میاوردن توی همزمانی. قبل از شروع یک تعریف اولیه ازشون بریم و ببینیم که اصلا چی هستن.
اکتور: یک پردازشگر مجازی هستش که لوکال state خصوصی خودشو داره. هر اکتور یک mailbox داره، وقتی مسیجی براش میاد و اکتور توی حالت idleهستش، بیدار میشه و مسیج ورودی شو پردازش میکنه. وقتی پردازشش تموم شد، مسیج های بعدی توی mailbox شو پردازش میکنه تا موقعی که خالیش کنه و دوباره بره توی مد sleep.
حین پردازش یک مسیج، اکتور میتونه اکتورهای دیگه ای رو بسازه، به اکتورهای دیگه مسیج بده و ...
پراسس: مفهوم عام تری از پردازشگر مجازی است که معمولا توسط سیستم عامل ایجاد میشه و پردازش خاصی رو انجام میده، پراسس ها میتونن طبق تعریف خاصی شبیه به اکتورها باشن که منظور ما در اینجا هم همینه.
ACTORS CAN ONLY BE CONCURRENT
چیزهایی که توی تعریف و ذات اکتورها نمیگنجه:
- یک چیز مشخص تحت کنترل وجود نداره. هیچ چیزی اسکژول نشده که توی استپ بعد اجرا بشه، و یا تبدیلگری وجود نداره که داده خامو به خروجی تبدیل کنه.
- تنها استیت موجود در سیستم توسط مسیج ها نگهداری میشه، و توی استیت لوکال اکتور ها میره، مسیج ها رو نمیشه پیش بینی کرد مگر زمانی که توسط گیرنده پراسس بشن، و استیت لوکال اکتورها از بیرون در دسترس نیست.
- مسیج ها یک طرفه اند، مفهومی مثل reply وجود نداره، اگر بخاید از اکتوری رسپانس بگیرید باید میل باکس خودتونو توی مسیج بزارید تا وقتی اکتور مسیجو پردازش کرد اونو براتون بفرسته.
- یک اکتور مسیج ها رو دونه دونه پردازش میکنه تا میل باکسشو خالی کنه، توی هر لحظه بیش از یک مسیج نمیتونه توسط اکتور پردازش بشه.
در نهایت، مجموعه ای از اکتورها به صورت همزمان و async کار میکنند و هیچ چیزی رو به اشتراک نمیزارن. اگر به اندازه کافی کور سی پی یو داشته باشید میتونید روی هر کدوم یک اکتور ران کنید وگرنه runtime هایی هستند که برامون context سیستم رو سوییچ میکنن و چندین اکتور رو همزمان و موازی اجرا میکنن.
و در هر صورت کدی که توی هر نوع اکتور اجرا میشه، یک کد یکسانه و ما چرا اومدیم سراغ اکتورها، چون به همزمانی برسیم.
· بیاید مشابه مثال پای سیب درس قبل رو بزنیم. در اینجا ما سه اکتور درگیر داریم: (مشتری، گارسون و سینی پای سیب)
فلوی کلی جریان مسیج ها به این شکل خاهد بود:
- مشتری درخواست پای سیب میده به گارسون
- گارسون از محل سینی پای سیب استعلام میگیره برای موجود بودن پای سیب درخواستی
- اگر به اندازه کافی موجود بود، سینی دار، پای سیب رو ارسال و به گارسون اعلام میکنه
- اگر هم موجودی کافی نباشه، به گارسون اطلاع میده و گارسون هم از مشتری عذرخواهی میکنه
ما این کدو با JSپیاده کردیم و با استفاده از Nact Library که با یک سری wrapper تونستیم به سادگی نوشتنه object ها اکتور بنویسیم به شکلی که key مسیج های دریافتی هست و value فانکشنی که بایستی اجرا بشه. خیلی از اکتور سیستم ها چیزی مشابه این رو ارائه میدن، فقط به زبون و سینتکس های متفاوت.
بیاید با مشتری شروع کنیم. یک مشتری میتونه سه مدل مسیج مختلف رو دریافت کنه:
- گرسنه بودن (که از مکانی خارج از دامین این مثال میاد مثلا دوستش بهش یاداوری میکنه گرسنه ای)
- پای سیب درخواستی موجوده (از سمت سینی دار پای میز ارسال میشه)
- متاسفم، پای سیبی نمونده (ارسالی از گارسون)
کد اکتور مشتری به این شکله:
const customerActor = {
'hungry for pie': (msg, ctx, state) => {
return dispatch(state.waiter,
{ type: "order", customer: ctx.self, wants: 'pie' })
},
'put on table': (msg, ctx, _state) =>
console.log(`${ctx.self.name} sees "${msg.food}" appear on the table`),
'no pie left': (_msg, ctx, _state) =>
console.log(`${ctx.self.name} sulks…`)
}
کد اکتور گارسون:
const waiterActor = {
"order": (msg, ctx, state) => {
if (msg.wants == "pie") {
dispatch(state.pieCase,
{ type: "get slice", customer: msg.customer, waiter: ctx.self })
}
else { console.dir(`Don't know how to order ${msg.wants}`);
}
},
"add to order": (msg, ctx) =>
console.log(`Waiter adds ${msg.food} to ${msg.customer.name}'s order`
),
"error": (msg, ctx) => {
dispatch(msg.customer, { type: 'no pie left', msg: msg.msg });
console.log(`\nThe waiter apologizes to ${msg.customer.name}:
${msg.msg}`)}
};
وقتی اکتوره گارسون یک مسیج مبنی بر order از مشتری دریافت میکنه، چک میکنه که سفارش پای سیبه یا نه و در صورتی که پای سیب باشه ارسالش میکنه به سینی دار (کسی که از سینی مراقبت میکنه و مسئولش هست) پای سیب و رفرنسها هم اگر مشاهده کنید توسط مسیج ها پاس داده میشه.
اکتور سینی دار، استیتش یک آرایه از اسلایس های پای سیبه. وقتی مسیج get slice رو دریافت میکنه از گارسون، نگاه میکنه که آیا موجودی براش باقی مونده، اگر آره اسلایس رو برای مشتری می فرسته و به گارسون هم اطلاع میده..
const pieCaseActor = {
'get slice': (msg, context, state) => {
if (state.slices.length == 0) { dispatch(msg.waiter, { type: 'error', msg: "no pie left", customer: msg.customer })
return state
}
else {
var slice = state.slices.shift() + " pie slice";
dispatch(msg.customer,
{ type: 'put on table', food: slice });
dispatch(msg.waiter,
{ type: 'add to order', food: slice, customer: msg.customer });
return state;
}
}
}
همونطور که ملاحظه کردید ما یکسری اکتور رو توسط اکتورهای دیگه ایجاد کردیم و بهشون initial state هم دادیم (رفرنس مشتری، سفارش و ...)، همچنین اکتورها میتونن داینامیک ایجاد و استارت بشن.
const actorSystem = start();
let pieCase = start_actor(
actorSystem,
'pie-case', pieCaseActor,
{ slices: ["apple", "peach", "cherry"] });
let waiter = start_actor(
actorSystem,'waiter',waiterActor,
{ pieCase: pieCase });
let c1 = start_actor(actorSystem, 'customer1',
customerActor, { waiter: waiter });
let c2 = start_actor(actorSystem, 'customer2',
customerActor, { waiter: waiter });
فرض کنید این کدو توی حالتی که دو تا مشتری گرسنه داریم که مشتری اول سه تا و مشتری دوم دو تا پای سیب درخواست میدن اجرا میکنیم:
dispatch(c1, { type: 'hungry for pie', waiter: waiter });
dispatch(c2, { type: 'hungry for pie', waiter: waiter });
dispatch(c1, { type: 'hungry for pie', waiter: waiter });
dispatch(c2, { type: 'hungry for pie', waiter: waiter });
dispatch(c1, { type: 'hungry for pie', waiter: waiter });
sleep(500)
.then(() => {
stop(actorSystem);
})
وقتی اجراش کنیم، ارتباط و درگیری بین اکتورها رو مشاهده خواهیم کرد و یکی از خروجی های متصور به این شکله:
$ node index.js
customer1 sees "apple pie slice" appear on the table
customer2 sees "peach pie slice" appear on the table
Waiter adds apple pie slice to customer1's order
Waiter adds peach pie slice to customer2's order
customer1 sees "cherry pie slice" appear on the table
Waiter adds cherry pie slice to customer1's order
The waiter apologizes to customer1: no pie left
customer1 sulks…
The waiter apologizes to customer2: no pie left
customer2 sulks…
NO EXPLICIT CONCURRENCY
توی اکتور مدل، نیازی نیست ما کدی رو برای هندل کردن همزمانی بزنیم، همچنین shared state ای هم وجود نداره. همچنین نیازی به زیرساخت و یا معماری سخت افزاری خاصی هم وجود نداره چون فریمورک ها اینو روی یک پردازشگر یا چندین پردازشگر لوکال یا توزیع شده میتونن پیاده سازی و اجرا کنن.
ERLANG SETS THE STAGE
زبون برنامه نویسی و ران تایم Erlang یک مثال خیلی خوب از مدل پیاده سازی اکتور بیس هست، در ارلنگ به اکتورها process میگن هرچند منظور از پراسس به اون شکلی که توی سیستم عامل پیاده میشه نیست، اما دقیقا مثل مکانیزم اکتورها که مثالشو زدیم، میتونیم توی ارلنگ میلیون ها پراسس رو روی یک ماشین ران کنید و دقیقا با مکانیزم مسیج باهم ارتباط برقرار میکنند و نسبت به هم ایزوله هستند. ران تایم ارلنگ مدلی از supervisor رو پیاده سازی کرده که این پراسس ها رو مانیتور و لایف تایم و مدیریت منابع و خطا شونو به عهده داره. همچنین hot-code loading داره، یعنی میتونید کد در حال اجرا رو ریپلیس کنید بدون اینکه پردازش تون رو متوقف کنه. (این رفتار خوراک سیستم هایی هست که همیشه باید انلاین باشن 99.999 درصد باید بالا باشن و به درخواست ها پاسخ بدن.
مدل پیاده سازی اکتور در اکثر زبون های برنامه نویسی کتابخونه و تردپارتی داره و به راحتی در دسترسه.)
دروس مرتبط: 28, 30, 36
منبع کانال تلگرامی: https://t.me/pragmaticprogrammer_fa