18

そもそも Erlang に惹かれたものの 1 つは Actor モデルです。異なるプロセスが同時に実行され、非同期メッセージングを介して相互作用するという考え。

私は、OTP に慣れ始めたばかりで、特に gen_server に注目しています。私が見たすべての例は、モジュールの動作を実装するhandle_call()のではなく、チュートリアルタイプの例であることを認めています。handle_cast()

私はそれが少し混乱していると思います。私が知る限りhandle_call、同期操作です。呼び出し元は、呼び出し先が完了して戻るまでブロックされます。これは、非同期メッセージ パッシングの哲学に反しているようです。

新しい OTP アプリケーションを開始しようとしています。これは基本的なアーキテクチャ上の決定のように思えるので、着手する前に理解しておく必要があります。

私の質問は次のとおりです。

  1. 実際には、人々は?handle_callではなくhandle_cast?を使用する傾向があります。
  2. もしそうなら、複数のクライアントが同じプロセス/モジュールを呼び出すことができる場合、スケーラビリティへの影響は何ですか?
4

4 に答える 4

24
  1. 状況によります。

    結果を得たい場合handle_callは、本当に一般的です。呼び出しの結果に関心がない場合は、 を使用してhandle_castください。を使用するhandle_callと、発信者はブロックします。はい。これで大抵は大丈夫です。例を見てみましょう。

    ファイルの内容をクライアントに返す Web サーバーがある場合は、複数のクライアントを処理できます。各クライアントファイルの内容が読み込まれるのを待たなければならないので、そのhandle_callようなシナリオでの使用はまったく問題ありません (愚かな例は別として)。

    リクエストを送信し、他の処理を行い、後で応答を取得する動作が本当に必要な場合は、通常、2 つの呼び出し (たとえば、結果を取得するための 1 つのキャストと 1 つの呼び出し) または通常のメッセージ パッシングが使用されます。しかし、これはかなりまれなケースです。

  2. を使用handle_callすると、通話中はプロセスがブロックされます。これにより、クライアントは応答を取得するためにキューに入れられるため、すべてが順番に実行されます。

    並列コードが必要な場合は、並列コードを作成する必要があります。これを行う唯一の方法は、複数のプロセスを実行することです。

要約すると、次のようになります。

  • を使用handle_callすると、呼び出し元がブロックされ、呼び出し中に呼び出されたプロセスが占有されます。
  • 並列アクティビティを継続したい場合は、並列化する必要があります。これを行う唯一の方法は、より多くのプロセスを開始することです。突然、呼び出しとキャストはそれほど大きな問題ではなくなりました (実際、呼び出しの方が快適です)。
于 2011-05-17T14:22:53.933 に答える
11

アダムの答えは素晴らしいですが、追加する点が1つあります

handle_call を使用すると、呼び出し中はプロセスがブロックされます。

これは、handle_call 呼び出しを行ったクライアントに常に当てはまります。これについて理解するのにしばらく時間がかかりましたが、これは必ずしもgen_serverがhandle_callに応答するときにブロックする必要があることを意味するわけではありません.

私の場合、gen_server を処理するデータベースを作成し、SELECT pg_sleep(10)PostgreSQL で「10 秒間スリープ」を意味する を実行するクエリを意図的に作成したときにこれに遭遇しました。私の課題: データベース gen_server がそこに座って、データベースが終了するのを待っているのは望ましくありません!

私の解決策は、gen_server:reply/2を使用することでした:

Module:handle_call/3 の戻り値で応答を定義できない場合、この関数を gen_server が call/2,3 または multi_call/2,3,4 を呼び出したクライアントに明示的に応答を送信するために使用できます。

コード内:

-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}).
于 2011-05-21T09:47:53.197 に答える
1

IMO、並行世界handle_callでは一般的に悪い考えです。プロセス A (gen_server) が何らかのイベント (ユーザーがボタンを押した) を受け取り、メッセージをプロセス B (gen_server) にキャストして、この押されたボタンの重い処理を要求しているとします。プロセス B はサブプロセス C をスポーンできます。サブプロセス C は、準備ができたらメッセージを A にキャストします (メッセージを A にキャストする B の)。処理時間中、A と B の両方が新しい要求を受け入れる準備ができています。A が C (または B) からキャスト メッセージを受信すると、たとえば結果がユーザーに表示されます。もちろん、2 番目のボタンが最初のボタンの前に処理される可能性があるため、A はおそらく適切な順序で結果を蓄積する必要があります。A と B をブロックするとhandle_call、このシステムはシングルスレッドになります (ただし、順序付けの問題は解決します)。

実際、スポーン C は に似てhandle_callいますが、違いは、C は高度に特殊化されており、「1 つのメッセージ」だけを処理して終了することです。B には他の機能 (ワーカー数の制限、タイムアウトの制御など) があるはずですが、そうでない場合は C が A から生成される可能性があります。

編集: C も非同期であるため、C の生成は似ていませhandle_callん (B はブロックされません)。

于 2011-05-17T16:28:02.473 に答える
0

これには2つの方法があります。1 つは、イベント管理アプローチの使用に変更することです。私が使用しているのは、示されているようにキャストを使用することです...

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

参照は、クエリを非同期的に追跡するために使用されます。

于 2013-05-28T02:14:10.320 に答える