Ge

GenServer در Elixir

4 تمرین

درباره‌ی GenServer

GenServer (سرور عمومی) یک رفتار است که تعاملات رایج کلاینت-سرور میان فرایندهای Elixir را انتزاع می‌کند.

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

Note

بدانید که اسم GenServer بار معنایی زیادی دارد. از آن برای توصیف ماژولی هم استفاده می‌شود که از رفتار GenServer استفاده می‌کند، و همچنین برای فرایندی که از ماژولی که از رفتار GenServer استفاده می‌کند راه‌اندازی شده است.

رفتار GenServer یک کال‌بک الزامی، init/1، و چند کال‌بک اختیاری جالب تعریف می‌کند: handle_call/3، handle_cast/2 و handle_info/3. قرار نیست _کلاینت‌ها_یی که از GenServer استفاده می‌کنند این کال‌بک‌ها را مستقیم فراخوانی کنند. در عوض، ماژول GenServer توابعی فراهم می‌کند که کلاینت‌ها می‌توانند با آن‌ها با یک فرایند GenServer ارتباط برقرار کنند.

غالباً یک ماژول واحد هم API کلاینت را تعریف می‌کند، یعنی مجموعه‌ای از توابع که سایر بخش‌های برنامه‌ی Elixir شما می‌توانند برای ارتباط با این فرایند GenServer فراخوانی کنند، و هم پیاده‌سازی‌های کال‌بک سرور را، که منطق این GenServer را در خود دارند.

بیایید اول نگاهی به یک مثال ساده از GenServer بیندازیم و بعد ببینیم هر کال‌بک چه معنایی دارد.

مثال

این یک سرور نمونه است که می‌تواند به پرسش‌های تکراری مسافران مزاحم در طول یک سفر جاده‌ای طولانی پاسخ دهد، دقیق‌تر بگوییم به این پرسش: «هنوز نرسیدیم؟». تعداد دفعاتی که این پرسش پرسیده شده را می‌شمارد و پاسخ‌هایی با دلخوری روزافزون برمی‌گرداند.

defmodule AnnoyingPassengerAutoresponder do
  use GenServer
  # Client API

  def start_link(init_arg) do
    GenServer.start_link(__MODULE__, init_arg)
  end

  def are_we_there_yet?(pid) do
    GenServer.call(pid, :are_we_there_yet?)
  end

  # Server callbacks

  @impl GenServer
  def init(_init_arg) do
    # the initial count of questions asked is always 0
    state = 0
    {:ok, state}
  end

  @impl GenServer
  def handle_call(:are_we_there_yet?, _from, state) do
    reply =
      cond do
        state <= 3 -> "No."
        state <= 10 -> "I told you #{state} times already. No."
        true -> "..."
      end

    # increase the count of questions asked
    new_state = state + 1
    # reply to the caller
    {:reply, reply, new_state}
  end
end

کال‌بک‌ها

init/1

یک سرور را می‌توان با فراخوانی GenServer.start/3 یا GenServer.start_link/3 راه‌اندازی کرد. تفاوت این توابع را در مفهوم پیوندها یاد گرفتیم.

این دو تابع:

  • یک ماژول را که رفتار GenServer را پیاده‌سازی می‌کند به عنوان آرگومان اول می‌پذیرند.
  • هر چیزی را به عنوان آرگومان دوم به اسم init_arg می‌پذیرند. همان‌طور که از اسمش برمی‌آید، این آرگومان به کال‌بک init/1 پاس داده می‌شود.
  • آرگومان سوم اختیاری را با گزینه‌های پیشرفته برای اجرای فرایند می‌پذیرند که الان به آن نمی‌پردازیم.

راه‌اندازی یک سرور با فراخوانی GenServer.start/3 یا GenServer.start_link/3 کال‌بک init/1 را به‌صورت مسدودکننده فراخوانی می‌کند. مقدار بازگشتی init/1 تعیین می‌کند که آیا سرور می‌تواند با موفقیت راه‌اندازی شود یا نه.

کال‌بک init/1 معمولاً یکی از این مقادیر را برمی‌گرداند:

  • {:ok, state}. سرور حلقه‌ی دریافت خود را با state به عنوان وضعیت اولیه‌اش آغاز می‌کند. state می‌تواند از هر نوعی باشد.
  • {:stop, reason}. reason می‌تواند از هر نوعی باشد. سرور حلقه‌ی دریافت خود را آغاز نمی‌کند. فرایند با دلیل داده‌شده خارج می‌شود.

امکانات پیشرفته‌تری هم وجود دارد که الان به آن‌ها نمی‌پردازیم.

اگر حلقه‌ی دریافت سرور آغاز شود، توابع GenServer.start/3 و GenServer.start_link/3 یک تاپل {:ok, pid} برمی‌گردانند. در غیر این صورت {:error, reason} را برمی‌گردانند

handle_call/3

پیامی که نیاز به پاسخ دارد را می‌توان با GenServer.call/2 به یک فرایند سرور فرستاد. این تابع به عنوان آرگومان اول، pid یک فرایند سرور در حال اجرا را انتظار دارد، و به عنوان آرگومان دوم، پیام را. پیام می‌تواند از هر نوعی باشد.

کال‌بک handle_call/3 مسئول رسیدگی و پاسخ دادن به پیام‌های همگام است. سه آرگومان دریافت می‌کند:

۱. message: مقداری که به عنوان آرگومان دوم به GenServer.call/2 پاس داده می‌شود. ۲. from: pid فرایندی که GenServer.call/2 را فراخوانی می‌کند. بیشتر اوقات می‌توان این آرگومان را نادیده گرفت. ۳. state: وضعیت فعلی سرور. به یاد داشته باشید که مقدار اولیه‌اش در کال‌بک init/1 تعیین شده بود.

کال‌بک handle_call/3 معمولاً یک تاپل ۳تایی به شکل {:reply, reply, state} برمی‌گرداند. یعنی عنصر دوم تاپل، reply که می‌تواند از هر نوعی باشد، به فراخواننده فرستاده می‌شود. عنصر سوم تاپل، state، وضعیت جدید سرور پس از رسیدگی به این پیام است.

امکانات پیشرفته‌تری هم وجود دارد که الان به آن‌ها نمی‌پردازیم.

Note

برای اینکه از روی اسم این کال‌بک یادتان بماند چه کاری می‌کند، به این فکر کنید که دارید کسی را با تلفن «call» می‌کنید.

اگر آن شخص در دسترس باشد، بلافاصله (به‌صورت همگام) پاسخ می‌گیرید.

handle_cast/2

پیامی که نیازی به پاسخ ندارد را می‌توان با GenServer.cast/2 به یک فرایند سرور فرستاد. آرگومان‌هایش همانند آرگومان‌های GenServer.call/2 هستند.

کال‌بک handle_cast/2 مسئول رسیدگی به این پیام‌ها است. دو آرگومان دریافت می‌کند، message و state، که همان آرگومان‌های کال‌بک handle_call/3 هستند (به‌جز from).

کال‌بک handle_cast/2 معمولاً یک تاپل ۲تایی به شکل {:noreply, state} برمی‌گرداند.

امکانات پیشرفته‌تری هم وجود دارد که الان به آن‌ها نمی‌پردازیم.

Note

برای اینکه از روی اسم این کال‌بک یادتان بماند چه کاری می‌کند، به یاد داشته باشید که «cast» معنای «پرتاب کردن» را هم می‌دهد.

اگر پیامی را در بطری به دریا بیندازید، انتظار ندارید بلافاصله پاسخ بگیرید، یا شاید هرگز.

از call استفاده کنم یا از cast؟

تقریباً همیشه از call استفاده کنید، حتی اگر کد کلاینت شما به پاسخ سرور نیازی نداشته باشد.

استفاده از call منتظر پاسخ می‌ماند، که به عنوان مکانیزمی برای اعمال فشار معکوس عمل می‌کند (تا مانع ارسال پیام‌های بیش‌ازحد کلاینت‌ها در یک لحظه شود). دریافت پاسخ از سرور همچنین تنها راه مطمئن شدن از این است که سرور پیام کلاینت را دریافت و رسیدگی کرده است.

handle_info/2

پیام‌ها می‌توانند به روش‌هایی غیر از فراخوانی GenServer.call/2 یا GenServer.cast/2 هم به صندوق ورودی سرور برسند، مثلاً با فراخوانی تابع ساده‌ی send/2.

برای رسیدگی به چنین پیام‌هایی، از کال‌بک handle_info/2 استفاده کنید. این کال‌بک دقیقاً به همان روش handle_cast/2 کار می‌کند.

رفتار GenServer یک پیاده‌سازی همه‌گیر از handle_info/2 فراهم می‌کند که خطاهای مربوط به پیام‌های غیرمنتظره را ثبت می‌کند. اگر آن پیاده‌سازی پیش‌فرض را بازنویسی کنید، مطمئن شوید که همیشه پیاده‌سازی همه‌گیر خودتان را هم بگنجانید. اگر فراموش کنید، سرور در صورت دریافت پیامی غیرمنتظره از کار می‌افتد.

مهلت‌های زمانی

مقدار بازگشتی هر یک از چهار کال‌بک توضیح‌داده‌شده در بالا را می‌توان با یک عنصر دیگر تاپل گسترش داد: یک مهلت زمانی. مثلاً به جای برگرداندن {:ok, state} از init/1، {:ok, state, timeout} را برگردانید.

از مهلت زمانی می‌توان برای تشخیص نبود پیام در صندوق پیام طی یک بازه‌ی زمانی مشخص استفاده کرد. اگر سرور از یکی از کال‌بک‌هایش یک مهلت زمانی برگرداند و تعداد میلی‌ثانیه‌ی مشخص‌شده بگذرد بدون آنکه پیامی برسد، کال‌بک handle_info/2 با :timeout به عنوان آرگومان اول فراخوانی می‌شود.

ویرایش از طریق GitHub این پیوند در پنجره یا زبانه‌ی جدیدی باز می‌شود

GenServer را یاد بگیرید

تمرین کردن قفل شده است

برای تمرین GenServer قفل 2 تمرین دیگر را باز کنید