Why GenServer.call is synchronous?

Hi, I try understand GenServer .
Why GenServer.call is synchronous ? I want to get, in asynchronous way, information about state of the pid, but I can’t.
For example if I send 1000 of GenServer.call(pid,:get) via :

[ Task.await(Task.async(fn -> Genserver.call(pid,:get) end)) , [ Task.await(Task.async(fn -> Genserver.call(pid,:get) end)) , … , … ]

when I get the last one I have to wait 999 times.

Maybe it is stupid question , but for me GenServer.call should be asynchronous because we want read state and GenServer.cast should be synchronous cause we modify the state (and we should lock state during modification) .
But the situation is opposite and I don’t understand why .
Thanks in advance for answer .

Cast and call are not about whether we read or modify the state, but do we wan’t to wait for the response, and do nothing or are we not going to wait for the response (we don’t care about the response at all).

Cast and call both can modify state.

Call is synchronous by nature, because it hast to send a response for which calling process await.

And beside that messages in GenServer are read sequentially IIRC, so any computation done based on message are done sequentially too.

4 Likes

There are two minor misconceptions here: call vs cast is not about read vs write. You can (and usually should) modify via call, and you often use call to read too. Call vs cast is literally about whether you as the client want to communicate async vs sync.

This becomes clearer if you think about the mechanism of communication: message passing. All message passing is async, so if you send a message to the genserver to read OR write and you want to know the result, the only way you can do so is to wait around for a reply message from the genserver. This is exactly what call does. If you want to write and you don’t care about the result, or if you want to tell the genserver to send you a reply and you’ll look at it later, you can consider cast.

The second misconception is that the genserver is some state that can be read directly by the client. As noted, all communication between processes is via message passing, and so if clients want to read a genserver, the genserver needs to send them a message. Genservers, like all processes, are single threaded, and so the message sending happens one after the next.

11 Likes

For some background this topic may be of some interest:

This may already be an indication that you are solving your problem in a less than optimal way in a process-oriented environment (there simply isn’t enough detail to know whether this is in fact true).

More often than not process state exists to enable the process to enact some sort of protocol in concert with other processes rather than the process simply acting as a container-of-state to be queried.

To a certain degree this has been a problem in some object-oriented designs and can be even more a problem with process-oriented solutions.

Some of my posts around “Tell, don’t Ask”:

You wouldn’t write code like that anyway. At the very least you would organize it more like:

funs = [fn -> Genserver.call(pid,:get) end, fn -> Genserver.call(pid,:get) end, … , … ]
tasks = Enum.map(funs, &Task.async/1)
results = Enum.map(tasks, &Task.await/2)

or

funs = [fn -> Genserver.call(pid,:get) end, fn -> Genserver.call(pid,:get) end, … , … ]
results =
  funs
  |> Enum.map(&Task.async/1)
  |> Enum.map(&Task.await/2)

The idea being that you first launch all the tasks and only then you start waiting on them (in this particular case it wouldn’t make much of a difference as you are waiting on the same process anyway).

And ideally you would be using Task.Supervisor.async_stream/6 anyway, as that gives you better control of the level of concurrency that may be reasonable under the circumstances.

2 Likes

Thanks for all answers.
So is there any possibility to get the state (if the process hold the state) from the process in asynchronous way ?
Anyway thanks for idea ““Tell, don’t Ask” , I’ll try change my thinking.

1 Like

@tomthestorm if you mean async with respect to the client, the client can simply do current_process = self(); GenServer.cast(pid, {:give_me_state, current_process}) and then the genserver can message the current process with its state.

If what you’re looking for is some kind of key value store that could be accessed concurrently by many processes, then that’s what an :ets table provides.

2 Likes

It may also help if you were to sketch out what kind of client side code you’re expecting here. It sort of sounds like you’re wondering if there is a way to get the Processes’s state without actually sending it a message, and no that is not possible. A GenServer is just a loop with a receive block, and if you want it to do something the only want to ask it is via a message.

2 Likes

What do you mean get it in an asynchronous way? How do you want the caller to receive the value?

Example: stack server with an asynchronous “pop” and a proxy server which turns it into a synchronous “pop”.

# file cast/lib/cast/application.ex
#
defmodule Cast.Application do
  use Application

  def start(_type, _args) do
    stack_server = :cast_stack
    proxy_server = :proxy

    children = [
      {Stack, [args: [:hello], name: stack_server]},
      {Proxy, [for_server: stack_server, name: proxy_server]}
    ]

    opts = [strategy: :rest_for_one, name: Cast.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
# file: cast/lib/stack.ex
# stack server with asynchronous "pop"
#
defmodule Stack do
  use GenServer

  # client

  def start_link(opts) do
    {args, opts} = Keyword.pop(opts, :args, [])
    {name, opts} = Keyword.pop(opts, :name, __MODULE__)
    opts = Keyword.put(opts, :name, name)
    GenServer.start_link(__MODULE__, args, opts)
  end

  def push(server, item), 
    do: GenServer.cast(server, {:push, item})

  def pop(server) do
    ref = make_ref()    # create a reference as a correlation identifier
    GenServer.cast(server, {:pop, ref, self()})
    ref
  end

  # implemention

  def pop_to([item | rest], ref, to) do
    Process.send_after(self(), {:pop_to, to, ref, item}, 500) # send head item in half a second
    rest
  end

  # callbacks

  @impl GenServer
  def init(stack),
    do: {:ok, stack}

  @impl GenServer
  def handle_cast({:pop, ref, to}, state),
    do: {:noreply, pop_to(state, ref, to)} # Part 1 of asynchronous "pop": pop the top and prepare to send it later.
  def handle_cast({:push, item}, state),
    do: {:noreply, [item | state]}         # push new item on the stack

  @impl GenServer
  def handle_info({:pop_to, to, ref, item}, state) do # process send_after message
    GenServer.cast(to, {:popped, ref, item})          # Part 2 of asynchronous "pop": send popped item back to client.
    {:noreply, state}
  end

end
# file: cast/lib/proxy.ex
# proxy server to the stack server that turns the
# stack server's asynchronous "pop" into a synchronous "pop".
#
defmodule Proxy do
  use GenServer

  # client

  def start_link(opts) do
    {for_server, opts} = Keyword.pop(opts, :for_server, [])
    {name, opts} = Keyword.pop(opts, :name, __MODULE__)
    opts = Keyword.put(opts, :name, name)
    GenServer.start_link(__MODULE__, {for_server, %{}}, opts)
  end

  def push(server, item),
    do: GenServer.cast(server, {:push, item})

  def pop(server),
    do: GenServer.call(server, :pop)

  # implementation
  def start_pop(from, {for_server, pending}) do
    ref = Stack.pop(for_server)               # cast asynchronous "pop" request
    new_pending = Map.put(pending, ref, from) # store the correlation identifier
    {for_server, new_pending}
  end

  def complete_pop(pending, ref, item) do
    case Map.fetch(pending, ref) do # find client based on correlation identifier
      {:ok, from} ->
        GenServer.reply(from, item) # now complete that pending "pop" call
        Map.delete(pending, ref)
      _ ->
        pending
    end
  end

  # callbacks

  @impl GenServer
  def init(args),
    do: {:ok, args}

  @impl GenServer
  def handle_call(:pop, from, state),
    do: {:noreply, start_pop(from, state)} # i.e. don't complete "pop" call; wait until _item_ is cast back

  @impl GenServer
  def handle_cast({:push, item}, {for_server, _} = state) do
    Stack.push(for_server, item) # forward item to stack "push"
    {:noreply, state}
  end
  def handle_cast({:popped, ref, item},{for_server, pending}) do # i.e. popped item has been cast back
    state = {for_server, complete_pop(pending, ref, item)}
    {:noreply, state}
  end

end
$ iex -S mix
Erlang/OTP 21 [erts-10.0] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]

Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Application.started_applications()
[
  {:cast, 'cast', '0.1.0'},
  {:logger, 'logger', '1.6.6'},
  {:mix, 'mix', '1.6.6'},
  {:iex, 'iex', '1.6.6'},
  {:elixir, 'elixir', '1.6.6'},
  {:compiler, 'ERTS  CXC 138 10', '7.2'},
  {:stdlib, 'ERTS  CXC 138 10', '3.5'},
  {:kernel, 'ERTS  CXC 138 10', '6.0'}
]
iex(2)> Application.stop(:cast)
:ok

16:46:40.552 [info]  Application cast exited: :stopped
iex(3)> r(Stack)
warning: redefining module Stack (current version loaded from _build/dev/lib/cast/ebin/Elixir.Stack.beam)
  lib/stack.ex:4

{:reloaded, Stack, [Stack]}
iex(4)> r(Proxy)
warning: redefining module Proxy (current version loaded from _build/dev/lib/cast/ebin/Elixir.Proxy.beam)
  lib/proxy.ex:5

{:reloaded, Proxy, [Proxy]}
iex(5)> r(Cast.Application)
warning: redefining module Cast.Application (current version loaded from _build/dev/lib/cast/ebin/Elixir.Cast.Application.beam)
  lib/cast/application.ex:3

{:reloaded, Cast.Application, [Cast.Application]}
iex(6)> Application.start(:cast)
:ok
iex(7)> Proxy.pop(:proxy)              # pop via proxy (synchronous)
:hello
iex(8)> Proxy.push(:proxy, :greetings) # push via proxy
:ok
iex(9)> Stack.push(:cast_stack, :bye)  # push directly
:ok
iex(10)> Stack.pop(:cast_stack)        # pop directly
#Reference<0.983023136.1924136961.112221>
iex(11)> flush()
:ok
iex(12)> Process.sleep(500)
:ok
iex(13)> flush()
{:"$gen_cast", {:popped, #Reference<0.983023136.1924136961.112221>, :bye}}
:ok
iex(14)> Stack.pop(:cast_stack)
#Reference<0.983023136.1924136961.112255>
iex(15)> Process.sleep(600)
:ok
iex(16)> flush()
{:"$gen_cast", {:popped, #Reference<0.983023136.1924136961.112255>, :greetings}}
:ok
iex(17)> 

I wish instead of just GenServer.call and GenServer.cast we also had a GenServer.async or something where it returns an opaque type that you can then later GenServer.await on to get the result. It would be really easy to make and would make interleaving work easier, but otherwise it would just be a wrapper around the GenServer.call protocol (async would send a message with a ref and pid, return that ref, then await would take that ref and receive on it to get the output). You can build that yourself with cast, but it would be nice to wrap the call protocol.

1 Like

Given the blocking nature, I don’t think the await concept is really appropriate inside a typical GenServer. What you want is a way to continue processing once the result becomes available - and there really isn’t that much ceremony involved in rigging something up.

# file: cast/lib/proxy.ex
# proxy server to the stack server that turns the
# stack server's asynchronous "pop" into a synchronous "pop".
#
defmodule Proxy do
  use GenServer

  # client

  def start_link(opts) do
    {for_server, opts} = Keyword.pop(opts, :for_server, [])
    {name, opts} = Keyword.pop(opts, :name, __MODULE__)
    opts = Keyword.put(opts, :name, name)
    GenServer.start_link(__MODULE__, {for_server, %{}}, opts)
  end

  def push(server, item),
    do: GenServer.cast(server, {:push, item})

  def pop(server),
    do: GenServer.call(server, :pop)

  # implementation

  # general continuation mechanism:
  # start_continue/3 and do_continue/3
  #
  # continued via handle_cast({:cont, ref, value}, state)
  #
  defp start_continue(cont, ref, fun),
    do: Map.put(cont, ref, fun)

  defp do_continue(cont, ref, value) do
    case Map.fetch(cont, ref) do # find continuation function
      {:ok, fun} ->
        fun.(value)                  # variant: return {:cont, ref, fun} to continue further 
        Map.delete(cont, ref)
      _ ->
        cont
    end
  end

  # start "pop" that needs to be continued later
  #
  defp start_pop(from, {for_server, cont}) do
    ref = Stack.pop(for_server, :cont)        # cast asynchronous "pop" request
    fun = make_reply_to(from)
    new_cont = start_continue(cont, ref, fun) # store a continuation function
    {for_server, new_cont}
  end

  defp make_reply_to(to) do
    fn item ->
      GenServer.reply(to, item)
    end
  end

  # callbacks

  @impl GenServer
  def init(args),
    do: {:ok, args}

  @impl GenServer
  def handle_call(:pop, from, state),
    do: {:noreply, start_pop(from, state)} # i.e. don't finish "pop" call; wait until _item_ is cast back

  @impl GenServer
  def handle_cast({:push, item}, {for_server, _} = state) do
    Stack.push(for_server, item) # forward item to stack "push"
    {:noreply, state}
  end
  def handle_cast({:cont, ref, value},{for_server, cont}) do # i.e. process via continuation function
    state = {for_server, do_continue(cont, ref, value)}
    {:noreply, state}
  end

end
# file: cast/lib/stack.ex
# stack server with asynchronous "pop"
#
defmodule Stack do
  use GenServer

  # client

  def start_link(opts) do
    {args, opts} = Keyword.pop(opts, :args, [])
    {name, opts} = Keyword.pop(opts, :name, __MODULE__)
    opts = Keyword.put(opts, :name, name)
    GenServer.start_link(__MODULE__, args, opts)
  end

  def push(server, item),
    do: GenServer.cast(server, {:push, item})

  def pop(server, as \\ :popped_item) do
    ref = make_ref()    # create a reference as a correlation identifier
    GenServer.cast(server, {:pop, self(), as, ref})
    ref
  end

  # implementation

  def pop_to([item | rest], to, as, ref) do
    Process.send_after(self(), {:cast_to, to, {as, ref, item}}, 500) # send head item in half a second
    rest
  end

  # callbacks

  @impl GenServer
  def init(stack),
    do: {:ok, stack}

  @impl GenServer
  def handle_cast({:pop, to, as, ref}, state),
    do: {:noreply, pop_to(state, to, as, ref)}  # Part 1 of asynchronous "pop": pop the top and prepare to send it later.
  def handle_cast({:push, item}, state),
    do: {:noreply, [item | state]}              # push new item on the stack

  @impl GenServer
  def handle_info({:cast_to, to, msg}, state) do # process send_after message
    GenServer.cast(to, msg)                      # Part 2 of asynchronous "pop": send popped value back to client.
    {:noreply, state}
  end

end

Not inside, rather for interacting with from outside, so you can submit a request to a genserver, do other work, then get the result, it’s the standard segregated send/receive set just in function form. :slight_smile:

Which Task.async/1, Task.await/2 already gives you - you just want to get rid of the extraneous process.

Yeah that would be odd to spool up a new process just to perform a call that the ‘call’ is already doing.

A GenServer.call is already doing what I proposed anyway:

Which calls:

I’m just proposing splitting it up so instead of performing the call and then receive-waiting for the response, it just breaks up those two steps. You can do that manually in the GenServer itself, but I’m not sure that actually belongs in the GenServer, it is the caller side that knows whether they want it all done in one call or an async/await after all, and it would just transparently work with gen_call’s regardless with no change to the server code at all.

This makes sense to me - when you invoke call the client process monitors the GenServer. If the GenServer crashes or you lose connection the client process will get an exit as well. We’d want the same behavior on await, and its these little details that people would often miss if they implement this on their own.

2 Likes

Indeed! :slight_smile: