OTP - синхронный и асинхронный обмен сообщениями

Отказ от ответственности: новичок erlang.

Одна из вещей, которая привлекла меня к erlang, в первую очередь, - модель Актера; идея о том, что разные процессы запускаются одновременно и взаимодействуют посредством асинхронной передачи сообщений.

Я только начинаю вставлять свои зубы в OTP и, в частности, смотреть на gen_server. Все примеры, которые я видел, и предоставили им примеры типов учебников, используйте handle_call() вместо handle_cast() для реализации поведения модуля.

Я нахожу это немного запутанным. Насколько я могу судить, handle_call - это синхронная операция: вызывающий объект блокируется до тех пор, пока собеседник не завершит и не вернется. Что, похоже, противоречит философии передачи асинхронных сообщений.

Я собираюсь запустить новое приложение OTP. Это похоже на фундаментальное архитектурное решение, поэтому я хочу быть уверенным, что понимаю, прежде чем приступать.

Чтобы быть конкретным, мои вопросы:

  • В реальной практике люди обычно используют handle_call, а не handle_cast?
  • Если да, то что влияет масштабируемость, когда несколько клиентов могут вызывать один и тот же процесс/модуль?

Спасибо.

Ответы

Ответ 1

  • Зависит от вашей ситуации.

    Если вы хотите получить результат, handle_call действительно распространен. Если вас не интересует результат вызова, используйте handle_cast. Когда используется handle_call, вызывающий блок блокирует, да. Это большая часть времени. Рассмотрим пример.

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

    Когда вам действительно нужно поведение отправки запроса, выполняя некоторую другую обработку, а затем получая ответ позже, обычно используются два вызова (например, один приведение и один вызов для получения результата) или обычное сообщение. Но это довольно редкий случай.

  • Использование handle_call блокирует процесс в течение всего времени вызова. Это приведет к тому, что клиенты будут стоять в очереди, чтобы получить ответы, и, следовательно, все будет работать последовательно.

    Если вам нужен параллельный код, вам нужно написать параллельный код. Единственный способ сделать это - запустить несколько процессов.

Итак, суммируем:

  • Использование handle_call будет блокировать вызывающего абонента и занять процесс, называемый длительностью вызова.
  • Если вы хотите, чтобы параллельные действия продолжались, вы должны распараллеливать. Единственный способ сделать это - запустить больше процессов, и вдруг вызов vs cast больше не является такой большой проблемой (на самом деле, это более удобно с вызовом).

Ответ 2

Ответ Адама велик, но я хочу добавить

Использование handle_call будет блокировать процесс в течение всего времени вызова.

Это всегда верно для клиента, который сделал вызов handle_call. Это заняло у меня некоторое время, чтобы обернуть голову, но это не обязательно означает, что gen_server также должен блокироваться при ответе на handle_call.

В моем случае я столкнулся с этим, когда создал сервер обработки данных gen_server и намеренно написал запрос, который выполнил SELECT pg_sleep(10), который является PostgreSQL-говорящим для "сна в течение 10 секунд", и был моим способом тестирования на очень дорогие запросы. Моя задача: я не хочу, чтобы база данных gen_server сидела там, ожидая завершения базы данных!

Моим решением было использовать gen_server: reply/2:

Эта функция может использоваться gen_server для явной отправки ответа клиенту, который вызвал call/2,3 или multi_call/2,3,4, когда ответ не может быть определен в возвращаемом значении модуля: handle_call/3.

В коде:

-module(database_server).
-behaviour(gen_server).
-define(DB_TIMEOUT, 30000).

<snip>

get_very_expensive_document(DocumentId) ->
    gen_server:call(?MODULE, {get_very_expensive_document, DocumentId}, ?DB_TIMEOUT).    

<snip>

handle_call({get_very_expensive_document, DocumentId}, From, State) ->     
    %% Spawn a new process to perform the query.  Give it From,
    %% which is the PID of the caller.
    proc_lib:spawn_link(?MODULE, query_get_very_expensive_document, [From, DocumentId]),    

    %% This gen_server process couldn't care less about the query
    %% any more!  It up to the spawned process now.
    {noreply, State};        

<snip>

query_get_very_expensive_document(From, DocumentId) ->
    %% Reference: http://www.erlang.org/doc/man/proc_lib.html#init_ack-1
    proc_lib:init_ack(ok),

    Result = query(pgsql_pool, "SELECT pg_sleep(10);", []),
    gen_server:reply(From, {return_query, ok, Result}).

Ответ 3

ИМО, в параллельном мире handle_call, как правило, плохая идея. Скажем, у нас есть процесс A (gen_server), получающий какое-то событие (пользователь нажал кнопку), а затем произнесение сообщения для обработки B (gen_server), требующего интенсивной обработки этой нажатой кнопки. Процесс B может порождать подпроцесс C, который, в свою очередь, возвращает сообщение A в готовность (от B, которое передает сообщение A тогда). Во время обработки оба A и B готовы принять новые запросы. Когда A принимает сообщение о трансляции из C (или B), например, отображает результат для пользователя. Конечно, возможно, что вторая кнопка будет обработана до первого, так что A должно, вероятно, накапливать результаты в правильном порядке. Блокировка A и B через handle_call сделает эту систему однопоточной (хотя и решит проблему порядка)

На самом деле, нереста C похожа на handle_call, разница в том, что C является узкоспециализированным, обрабатывает только "одно сообщение" и выходит после этого. Предполагается, что B имеет другую функциональность (например, ограничение числа рабочих, тайм-ауты управления), в противном случае C можно было бы породить из A.

Изменить: C также асинхронно, поэтому нерестится C, он не похож на handle_call (B не заблокирован).

Ответ 4

Есть два способа сделать это. Один из них - изменить подход к управлению событиями. Тот, который я использую, заключается в использовании приведения, как показано...

    submit(ResourceId,Query) ->
      %%
      %% non blocking query submission
      %%
      Ref = make_ref(),
      From = {self(),Ref},
      gen_server:cast(ResourceId,{submit,From,Query}),
      {ok,Ref}.

И код трансляции/отправки...

    handle_cast({submit,{Pid,Ref},Query},State) ->
      Result = process_query(Query,State),
      gen_server:cast(Pid,{query_result,Ref,Result});

Ссылка используется для отслеживания запроса асинхронно.