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});
Ссылка используется для отслеживания запроса асинхронно.