Эликсир неблокирующих потоков на GenServer? - PullRequest
0 голосов
/ 15 декабря 2018

Я пытаюсь выполнить простую задачу, но у меня огромные трудности.

Предположим, у меня есть GenServer, и один из его обратных вызовов выглядит следующим образом:

  @impl true
  def handle_call(:state, _, state) do
    # Something that would require 10 seconds
    newState = do_job()
    {:reply, newState, newState}
  end

Если я прав, вызов GenServer.call(:server, :state) со стороны клиента заблокирует сервер на 10 секунд, а затем новое состояние будет возвращено клиенту.

Окей.Я хочу, чтобы сервер справился с этой задачей, не будучи заблокированным.Я пытался использовать Задачи, но Task.await/2 и Task.yield/2 блокируют сервер.

Я хочу, чтобы сервер не блокировал, и через эти 10 секунд получил результат на клиентском терминале.Как это возможно?

1 Ответ

0 голосов
/ 15 декабря 2018

Если я прав, вызов GenServer.call (: server,: state) со стороны клиента блокирует сервер на 10 секунд, а затем новое состояние возвращается клиенту.

Да.Elixir делает то, что вы говорите, и в этой строке:

newState = do_job()

вы говорите elixir назначить возвращаемое значение do_job() переменной newState.Единственный способ, которым elixir может выполнить это назначение, - получить возвращаемое значение go_job() ...., что займет 10 секунд.

Я хочу, чтобы сервер не блокировался, и после этих 10 секундполучить результат на клиентском терминале.

Один из подходов заключается в том, чтобы GenServer spawn() запустил новый процесс, чтобы выполнить 10-секундную функцию и передать pid клиента новому процессу.Когда новый процесс получает возвращаемое значение из 10-секундной функции, новый процесс может send() сообщение клиенту, используя клиентский pid.

Это означает, что клиент должен будет вызвать handle_call(), а неhandle_cast(), поскольку серверная реализация handle_cast() не имеет переменной параметра from, содержащей клиентский pid.С другой стороны, handle_call() действительно получает клиентский pid в переменной параметра from, поэтому сервер может передать клиентский pid порожденному процессу.Обратите внимание, что spawn() возвращается немедленно, что означает, что handle_call() может немедленно вернуться с ответом, подобным :working_on_it.

Следующая проблема: как клиент узнает, когда новый процесс, который породил GenServer, имеетзакончил выполнение функции 10 секунд?Клиент не может знать, когда какой-либо посторонний процесс на сервере завершил выполнение, поэтому клиент должен ждать в получении, пока не прибудет сообщение от порожденного процесса.И, если клиент проверяет сообщения в своем почтовом ящике, было бы полезно узнать, кто был отправителем, что означает, что handle_call() также должен вернуть клиенту pid порожденного процесса.Другим вариантом для клиента является опрос своего почтового ящика каждый раз между приступами выполнения другой работы.Для этого клиент может определить получение с коротким тайм-аутом в после предложения , затем вызвать функцию в after clause для выполнения некоторой клиентской работы, после чего следует рекурсивный вызов функции, содержащейполучить, чтобы функция снова проверила почтовый ящик.

А как насчет Task?Согласно документам Task :

Если вы используете асинхронные задачи, вы должны ждать ответа ...

Ну, тогда что хорошего в асинхронном задании, если нужно подождать?Ответ: если у процесса есть хотя бы двух долго выполняющихся функций, которые ему необходимо выполнить, то процесс может использовать Task.async() для одновременного запуска всех функций, вместо того, чтобы выполнять одну функцию и ждать, пока она не будетзавершается, затем выполняет другую функцию и ждет, пока она не завершится, затем выполняет другую и т. д.

Но Task также определяет функцию start () :

start (mod, fun, args)

Запускает задание.

Используется только в том случае, если задание используется для побочных эффектов (т.е. не интересуетвозвращаемый результат), и он не должен быть связан с текущим процессом.

Звучит так, будто Task.start() завершает то, что я описал в первом подходе.Вам необходимо определить fun, чтобы он выполнял 10-секундную функцию, а затем отправить сообщение обратно клиенту после завершения 10-секундной функции (= побочный эффект ).

Ниже приведен простой пример GenServer, который порождает долго выполняющуюся функцию, которая позволяет серверу оставаться отзывчивым на запросы других клиентов, пока выполняется долго работающая функция:

a.exs:

defmodule Gen1.Server do
  use GenServer

  @impl true
  def init(init_state) do
    {:ok, init_state}
  end

  def long_func({pid, _ref}) do
    Process.sleep 10_000
    result = :dog
    send(pid, {self(), result})
  end

  @impl true
  def handle_call(:go_long, from, state) do
    long_pid = spawn(Gen1.Server, :long_func, [from])
    {:reply, long_pid, state}
  end
  def handle_call(:other, _from, state) do
    {:reply, :other_stuff, state}
  end

end

Сессией iex будет клиент:

~/elixir_programs$ iex a.exs
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> {:ok, server_pid} = GenServer.start_link(Gen1.Server, [])
{:ok, #PID<0.93.0>}

iex(2)> long_pid = GenServer.call(server_pid, :go_long, 15_000)
#PID<0.100.0>

iex(3)> GenServer.call(server_pid, :other)                       
:other_stuff

iex(4)> receive do                                             
...(4)> {^long_pid, reply} -> reply                            
...(4)> end                                                    
:dog

iex(7)> 

Переменная типа long_pid будет соответствовать чему угодно.Чтобы long_pid соответствовал только его текущему значению, вы указываете ^long_pid (^ называется оператором вывода).

GenServer также позволяет блокировать вызов клиента на handle_call(), в то же время позволяя серверу продолжить выполнение.Это полезно, если клиент не может продолжать работу до тех пор, пока не получит некоторые необходимые данные с сервера, но вы хотите, чтобы сервер оставался отзывчивым на других клиентов.Вот пример этого:

defmodule Gen1.Server do
  use GenServer

  @impl true
  def init(init_state) do
    {:ok, init_state}
  end

  @impl true
  def handle_call(:go_long, from, state) do
    spawn(Gen1.Server, :long_func, [from])
    {:noreply, state}  #The server doesn't send anything to the client, 
                       #so the client's call of handle_call() blocks until 
                       #somebody calls GenServer.reply().
  end

  def long_func(from) do
    Process.sleep 10_000
    result = :dog
    GenServer.reply(from, result) 
  end

end

В iex:

iex(1)> {:ok, server_pid} = GenServer.start_link(Gen1.Server, [])
{:ok, #PID<0.93.0>}

iex(2)> result = GenServer.call(server_pid, :go_long, 15_000)
...hangs for 10 seconds...   
:dog

iex(3)> 
...