OTP - синхронный и асинхронный обмен сообщениями - PullRequest
16 голосов
/ 17 мая 2011

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

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

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

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

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

Если говорить конкретно, мои вопросы таковы:

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

Спасибо.

Ответы [ 4 ]

23 голосов
/ 17 мая 2011
  1. Зависит от вашей ситуации.

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

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

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

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

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

Итак, подведем итог:

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

Ответ Адама великолепен, но у меня есть одно замечание, чтобы добавить

Использование 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, когда ответ не может быть определен в возвращаемом значении Module: 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 голос
/ 17 мая 2011

ИМО, в параллельном мире 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 не блокируется).

0 голосов
/ 28 мая 2013

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

    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});

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...