14

init/1プロセスの関数gen_serverが初期化する必要があることを示すメッセージをプロセス自体に送信するというパターンを時折見てきました。これの目的は、gen_serverプロセスを生成するプロセスが待機する必要がないように、プロセス自体を非同期的に初期化することです。次に例を示します。

-module(test).
-compile(export_all).

init([]) ->
    gen_server:cast(self(), init),
    {ok, {}}.

handle_cast(init, {}) ->
    io:format("initializing~n"),
    {noreply, lists:sum(lists:seq(1,10000000))};
handle_cast(m, X) when is_integer(X) ->
    io:format("got m. X: ~p~n", [X]),
    {noreply, X}.

b() ->
    receive P -> {} end,
    gen_server:cast(P, m),
    b().

test() ->
    B = spawn(fun test:b/0),
    {ok, A} = gen_server:start_link(test,[],[]),
    B ! A.

initこのプロセスは、メッセージが他のメッセージよりも前に受信されることを前提としています。そうでない場合、クラッシュします。このプロセスがメッセージのm前にinitメッセージを取得することは可能ですか?


によって生成されたランダムな pid にメッセージを送信するプロセスがないと仮定しましょうlist_to_pid。これを行うアプリケーションは、この質問に対する答えに関係なく、おそらくまったく機能しないからです。

4

5 に答える 5

5

プロセスが初期化メッセージの前にメッセージを取得することは可能ですか?という質問に対する理論的な答えは? はい。_ しかし、実際には (どのプロセスも list_to_pid を実行してメッセージを送信していない場合) 、gen_server が登録済みプロセスでない場合、このプロセスに対する答えはNOです。

これは、gen_server:start_link が返されることで、gen_server のコールバック init が確実に実行されるためです。したがって、初期化メッセージは、他のプロセスが Pid を取得してメッセージを送信する前に、プロセス メッセージ キュー内の最初のメッセージになります。したがって、プロセスは安全であり、init の前に他のメッセージを受け取ることはありません。

ただし、コールバック init 関数が完了する前であっても、登録された名前を使用して gen_server にメッセージを送信している可能性のあるプロセスが存在する可能性があるため、登録されたプロセスには同じことは当てはまりません。このテスト関数を考えてみましょう。

test() ->
    Times = lists:seq(1,1000),
    spawn(gen_server, start_link,[{local, ?MODULE}, ?MODULE, [], []]),
    [gen_server:cast(?MODULE, No) || No <-Times].

サンプル出力は

1> async_init:test().
Received:356
Received:357
[ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,
 ok,ok,ok,ok,ok,ok,ok,ok,ok,ok|...]
Received:358
Received:359
2> Received:360
2> Received:361
...
2> Received:384
2> Received:385
2> Initializing
2> Received:386
2> Received:387
2> Received:388
2> Received:389 
...

初期化前に gen_server が 356 から 385 メッセージを受信したことがわかります。したがって、非同期コールバックは、登録名のシナリオでは機能しません。

これは2つの方法で解決できます

1.Pidが返却された後、プロセスを登録します。

 start_link_reg() ->
      {ok, Pid} = gen_server:start(?MODULE, [], []),
      register(?MODULE, Pid).

2.または、init メッセージの handle_cast にプロセスを登録します。

handle_cast(init, State) ->
    register(?MODULE, self()),
    io:format("Initializing~n"),
    {noreply, State};

この変更後の出力例は次のとおりです。

1> async_init:test().
Initializing
Received:918
Received:919
[ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,
 ok,ok,ok,ok,ok,ok,ok,ok,ok,ok|...]
Received:920
2> Received:921
2> Received:922
...

したがって、初期化のために自分自身にメッセージを送信しても、それが最初に受信するメッセージであるとは限りませんが、コード (および設計) を少し変更することで、最初に実行されることを保証できます。

于 2013-09-21T03:44:18.460 に答える
2

この特定のケースでは、'm' の前に 'init' メッセージが受信されるという前提で安全です。ただし、一般的に(特にプロセスを登録する場合)、これは正しくありません。

init コードが最初に実行されることを知って 100% 安全になりたい場合は、次のようにすることができます。

start_link(Args...) ->
    gen_server:start_link(test, [self(), Args...], []).

init([Parent, Args...]) ->
    do_your_synchronous_start_stuff_here,
    proc_lib:init_ack(Parent, {ok, self()}),
    do_your_async_initializing_here,
    io:format("initializing~n"),
    {ok, State}.

私はこれをテストしなかったので、「おまけ」の init_ack が端末に醜いメッセージを出力するかどうかはわかりません。その場合、コードを少し拡張する必要がありますが、一般的な考え方は変わりません。お知らせください。回答を更新します。

于 2013-08-01T17:50:18.893 に答える
1

サンプル コードは安全で、m常に の後に受け取りinitます。

ただし、理論的な観点からinit/1は、gen_server のハンドラがgen_server:cast/2send プリミティブを使用してメッセージを自分自身に送信した場合、それが最初のメッセージであるとは限りません。

init/1は gen_server のプロセス内で実行されるため、これを保証する方法はありません。したがって、プロセスが作成されて pid とメールボックスが割り当てられた後です。非 SMP モードでは、スケジューラは init 関数が呼び出される前、またはメッセージが送信される前に、ある程度の負荷がかかった状態でプロセスをスケジュールすることができgen_server:cast/2ます。エミュレーターは、他のプロセスに時間を割く時が来たかどうかをテストします。SMP モードでは、プロセスにメッセージを送信するコードを実行する別のスケジューラを使用できます。

理論と実践を区別するのは、プロセスの存在を知る方法です (メッセージの前にinitメッセージを送信するため)。コードは、スーパーバイザーからのリンク、登録名、返されたプロセスのリストを使用するerlang:processes()か、ランダムな値で呼び出すか、 でlist_to_pid/1pid を非シリアル化することさえできbinary_to_term/1ます。ノードは、特に作成番号が 3 の後にラップアラウンドすることを考えると、シリアル化された pid を持つ別のノードからメッセージを受け取ることさえあるかもしれません (別の質問の間違ったプロセスが他のノードで強制終了されましたか?を参照してください)。

これは実際にはほとんどありません。その結果、実用的な観点から、このパターンが使用されるたびに、メッセージが最初に受信され、他のメッセージを受信する前にサーバーが初期化されるようにコードを設計できます。init

gen_server が登録済みのプロセスである場合は、スーパーバイザーから開始し、すべてのクライアントが後でスーパーバイザー ツリーで開始されるようにするか、ある種の (おそらく下位の) 同期メカニズムを導入します。これは、非同期初期化のこのパターンを使用しない場合でも必要です (そうしないと、クライアントはサーバーに到達できません)。もちろん、この gen_server のクラッシュと再起動の場合にはまだ問題が発生する可能性がありますが、これはシナリオに関係なく当てはまり、慎重に作成された監視ツリーによってのみ保存できます。

gen_server が登録されていないか、名前で参照されていない場合、クライアントは最終的に pid を渡すgen_server:call/2,3gen_server:cast/2、 を呼び出すスーパーバイザーを通じて取得しますgen_server:start_link/3gen_server:start_link/3返されたときにのみ返されるinit/1ため、initメッセージがキューに入れられた後にのみ返されます。これはまさに上記のコードが行うことです。

于 2013-09-18T15:32:10.307 に答える
0

これは100%安全ではありません。117 ~ 129 行で、次gen.erlのことがわかります。

init_it(GenMod, Starter, Parent, Mod, Args, Options) ->
init_it2(GenMod, Starter, Parent, self(), Mod, Args, Options).

init_it(GenMod, Starter, Parent, Name, Mod, Args, Options) ->
    case name_register(Name) of
        true ->
            init_it2(GenMod, Starter, Parent, Name, Mod, Args, Options);
        {false, Pid} ->
            proc_lib:init_ack(Starter, {error, {already_started, Pid}})
    end.

init_it2(GenMod, Starter, Parent, Name, Mod, Args, Options) ->
    GenMod:init_it(Starter, Parent, Name, Mod, Args, Options).

init_it/7プロセスでは、最初に名前を登録してから関数を呼び出す呼び出しを行いinit_it2/7ます。GenMod:init_it/6init/1

ただし、gen_server:start_link が戻る前に、新しいプロセス ID を推測することはほとんどありません。ただし、登録された Nameでサーバーにメッセージを送信し、メッセージが gen_server:cast が呼び出される前に到着した場合、コードは間違っています。

ダニエルの解決策は正しいかもしれませんが、2 つproc_lib:init_ackがエラーを引き起こすかどうかはよくわかりません。ただし、親は予期しないメッセージを受け取りたくありません。>_<

ここに別の解決策があります。サーバーが初期化されているかどうかを示すフラグgen_servser 状態に保持します。を受信したらm、サーバーが初期化されているかどうかを確認します。そうでない場合は、gen_castmを自分に送信します。

これは少し面倒な解決策ですが、正しいと確信しています。=_=

私はここの新入生です。コメントを追加できればいいのにと思います。>"<

于 2013-08-01T18:01:36.777 に答える