#erlang #erlang-otp
#эрланг #erlang-otp #erlang
Вопрос:
Одна из вещей, которая привлекла меня в Erlang в первую очередь, — это модель актора; идея о том, что различные процессы выполняются одновременно и взаимодействуют посредством асинхронного обмена сообщениями.
Я только начинаю вникать в OTP и, в частности, смотрю на gen_server. Все примеры, которые я видел — и при условии, что они являются примерами типа tutorial — используют handle_call()
, а не handle_cast()
для реализации поведения модуля.
Я нахожу это немного сбивающим с толку. Насколько я могу судить, handle_call
это синхронная операция: вызывающий блокируется до тех пор, пока вызываемый не завершит и не вернется. Что, по-видимому, противоречит философии асинхронной передачи сообщений.
Я собираюсь запустить новое OTP-приложение. Это кажется фундаментальным архитектурным решением, поэтому я хочу быть уверенным, что понимаю, прежде чем приступать.
Мои вопросы таковы:
- На реальной практике люди склонны использовать
handle_call
, а неhandle_cast
? - Если да, то каково влияние на масштабируемость, когда несколько клиентов могут вызывать один и тот же процесс / модуль?
Ответ №1:
-
Зависит от вашей ситуации.
Если вы хотите получить результат,
handle_call
это действительно распространенное явление. Если вас не интересует результат вызова, используйтеhandle_cast
. Когдаhandle_call
используется, вызывающий будет блокировать, да. В большинстве случаев это нормально. Давайте взглянем на пример.Если у вас есть веб-сервер, который возвращает содержимое файлов клиентам, вы сможете обрабатывать несколько клиентов. Каждому клиенту приходится ждать, пока содержимое файлов будет прочитано, поэтому использование
handle_call
в таком сценарии было бы совершенно нормально (кроме глупого примера).Когда вам действительно нужно поведение отправки запроса, выполнения некоторой другой обработки и последующего получения ответа позже, обычно используются два вызова (например, одно приведение и один вызов для получения результата) или обычная передача сообщения. Но это довольно редкий случай.
-
Использование
handle_call
заблокирует процесс на время вызова. Это приведет к тому, что клиенты встанут в очередь, чтобы получить свои ответы, и, таким образом, все это будет выполняться последовательно.Если вам нужен параллельный код, вы должны написать параллельный код. Единственный способ сделать это — запустить несколько процессов.
Итак, подведем итог:
- Использование
handle_call
заблокирует вызывающего и займет вызываемый процесс на время вызова. - Если вы хотите, чтобы параллельные действия продолжались, вы должны распараллелить. Единственный способ сделать это — запустить больше процессов, и внезапно call вместо cast больше не является такой большой проблемой (на самом деле, с call удобнее).
Ответ №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's 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}).
Комментарии:
1. Более краткое объяснение вышесказанного также здесь: trapexit.org/Building_Non_Blocking_Erlang_apps
2. Спасибо за ответ @асимптота. Шаблон имеет смысл: по сути, gen_server становится «диспетчером» с работой, выполняемой в порожденных дочерних элементах (что немного похоже на сценарий @Victor «подпроцесс C», если я правильно понимаю?
3. @sfinnie: Именно. Тем не менее, эта модель открывает доступ к большому количеству червей, и я опустил много важных деталей. 1) Поскольку мы (правильно) подключили функцию spawn_link() к дочерним «рабочим», если дочерние элементы неожиданно завершат работу, gen_server выйдет из строя! Это никуда не годится, и мы, вероятно, захотим перехватывать сообщения о выходе на gen_server и явно обрабатывать их в handle_info. 2) Что, если одновременно поступает слишком много запросов? Вероятно, мы хотим отслеживать, сколько запросов не выполнено, и отклонять / повторять последующие запросы.
4. Кроме того, 3) что, если многие запросы завершаются сбоем один за другим? Это может указывать на проблему с серверной частью базы данных, и последнее, что мы хотим сделать, это разрешить одинаковую скорость поступления входящих запросов в базу данных. В этом сценарии требуется «автоматический выключатель»; в случае резкого увеличения числа сбоев мы намеренно говорим клиентам вернуться позже и предоставить серверной части передышку. У «Release it!» есть и другие отличные идеи: pragprog.com/titles/mnee/release-it
Ответ №3:
IMO, в параллельном мире 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 не заблокирован).
Комментарии:
1. Спасибо @Victor. Это тоже было моим предположением. Вопрос возник именно потому, что все примеры для gen_server, похоже, используют
handle_call
. Преимущество синхронного кода в том, что за ним легче следить и сохраняется порядок; недостатком является блокирующее поведение. Полагаю, лошади для курсов. Повторное замечание. C, по сути, является синхронным вызовом.
Ответ №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});
Ссылка используется для асинхронного отслеживания запроса.