2016-12-13 11 views
7

Ich weiß, dass es ziemlich unmöglich ist, dass ein GenServer-Prozess sich selbst aufruft, weil Sie im Wesentlichen einen Deadlock haben. Aber ich bin neugierig, ob es einen bevorzugten Weg gibt, so etwas zu tun.Ordnungsgemäße Methode zum Strukturieren von GenServer-Aufrufe an self

Angenommen, das folgende Szenario: Ich habe eine Warteschlange, aus der ich Sachen herauspoppe. Wenn die Warteschlange jemals leer ist, möchte ich sie wieder auffüllen. Ich könnte es so strukturieren:

def handle_call(:refill_queue, state) do 
    new_state = put_some_stuff_in_queue(state) 
    {:reply, new_state} 
end 

def handle_call(:pop, state) do 
    if is_empty_queue(state) do 
    GenServer.call(self, :refill_queue) 
    end 

    val,new_state = pop_something(state) 

    {:reply, val, new_state} 
end 

Das große Problem hier ist, dass dies Deadlock wird, wenn wir versuchen, die Warteschlange aufzufüllen. Eine Lösung, die ich in der Vergangenheit verwendet habe, ist die Verwendung von cast mehr, damit es nicht blockiert. Wie so (call für Nachfüllung cast ändern)

def handle_cast(:refill_queue, state) do 

Aber in diesem Fall, ich denke, es wird nicht funktionieren, da die Asynchron-Guss die Warteschlange zum Nachfüllen im pop Fall zurückkehren könnte, bevor sie tatsächlich die Warteschlange Bedeutung Füllung Ich werde versuchen, eine leere Warteschlange zu löschen. Die Kernfrage ist: Wie ist der beste Weg, damit umzugehen? Ich nehme an, die Antwort ist nur put_some_stuff_in_queue direkt innerhalb der pop Anruf anrufen, aber ich wollte überprüfen. Mit anderen Worten, es scheint, als ob das Richtige zu tun ist, machen handle_call und handle_cast so einfach wie möglich und im Grunde nur Wrapper zu anderen Funktionen, wo die eigentliche Arbeit passiert. Dann erstellen Sie so viele handle_* Funktionen, wie Sie alle möglichen Fälle abdecken müssen, mit denen Sie beschäftigen werden, anstatt handle_call(:foo) wiederum handle_call(:bar) anrufen.

+2

Ich würde 'refill_queue' eine Plain-Funktion machen und sie von' handle_call (: pop) 'aufrufen, wenn sie synchron sein muss. Andernfalls haben Sie mehrere Möglichkeiten, async zu bearbeiten (senden Sie eine andere Nachricht an self, haben Sie einen anderen Prozess, der das Nachfüllen erledigt, etc.) –

+3

Sie sollten sich die [GenStage] (http://elixir-lang.org/blog) anschauen/2016/07/14/annouting-genstage /), klingt, als ob es die Funktionalität bietet, nach der Sie suchen. – mudasobwa

Antwort

5

Es gibt eine Funktion in GenServer Modul namens reply/2. Das zweite Argument des Callbacks handle_call/3 ist die Verbindung zum Client. Sie können einen neuen Prozess zur Verarbeitung der Verbindung erstellen und {:noreply, state} in der Callback-Klausel zurückgeben. Mit Ihrem Beispiel:

defmodule Q do 
    use GenServer 

    ############ 
    # Public API 

    def start_link do 
    GenServer.start_link(__MODULE__, []) 
    end 

    def push(pid, x) do 
    GenServer.call(pid, {:push, x}) 
    end 

    def pop(pid) do 
    GenServer.call(pid, :pop) 
    end 

    ######## 
    # Helper 

    # Creates a new process and does a request to 
    # itself with the message `:refill`. Replies 
    # to the client using `from`. 
    defp refill(from) do 
    pid = self() 
    spawn_link fn -> 
     result = GenServer.call(pid, :refill) 
     GenServer.reply(from, result) 
    end 
    end 

    ########## 
    # Callback 

    def handle_call(:refill, _from, []) do 
    {:reply, 1, [2, 3]} 
    end 
    def handle_call(:refill, _from, [x | xs]) do 
    {:reply, x, xs} 
    end 
    def handle_call({:push, x}, _from, xs) when is_list(xs) do 
    {:reply, :ok, [x | xs]} 
    end 
    def handle_call(:pop, from, []) do 
    # Handles refill and the reply to from. 
    refill(from) 
    # Returns nothing to the client, but unblocks the 
    # server to get more requests. 
    {:noreply, []} 
    end 
    def handle_call(:pop, _from, [x | xs]) do 
    {:reply, x, xs} 
    end 
end 

Und Sie würden das folgende erhalten:

iex(1)> {:ok, pid} = Q.start_link() 
{:ok, #PID<0.193.0>} 
iex(2)> Q.pop(pid) 
1 
iex(3)> Q.pop(pid) 
2 
iex(4)> Q.pop(pid) 
3 
iex(5)> Q.pop(pid) 
1 
iex(6)> Q.pop(pid) 
2 
iex(7)> Q.pop(pid) 
3 
iex(8)> Q.push(pid, 4) 
:ok 
iex(9)> Q.pop(pid)  
4 
iex(10)> Q.pop(pid) 
1 
iex(11)> Q.pop(pid) 
2 
iex(12)> Q.pop(pid) 
3 
iex(13)> tasks = for i <- 1..10 do 
...(13)> Task.async(fn -> {"Process #{inspect i}", Q.pop(pid)} end) 
...(13)> end 
(...) 
iex(14)> for task <- tasks, do: Task.await(task) 
[{"Process 1", 1}, {"Process 2", 2}, {"Process 3", 1}, {"Process 4", 2}, 
{"Process 5", 3}, {"Process 6", 3}, {"Process 7", 2}, {"Process 8", 1}, 
{"Process 9", 1}, {"Process 10", 3}] 

So ist es in der Tat möglich, dass ein GenServer Anforderungen an sich selbst zu tun. Du musst nur wissen wie.

Ich hoffe, das hilft.

+0

Ja, ich denke, das ist ziemlich genau das, was ich mich gefragt habe. Ich habe mich gefragt, wofür das Argument 'from' verwendet wurde, und jetzt macht es mehr Sinn. – Micah

+3

Ich hatte keine Ahnung, dass Sie später antworten könnten, während Sie andere Nachrichten bearbeiten, was großartig ist zu wissen. ABER! Wird es nicht möglich sein, dass mehrere Pop-Calls gleichzeitig den Refill anrufen? Ich meine, wenn das Refill 10ms dauert und wir alle 3ms einen Pop-Call haben, bekommen wir 2 Pops (jedes löst einen weiteren Refill aus), während der erste den Refill macht. Weil diese Nachfüllung in die Nachrichtenwarteschlange geht, um nach den Knallen bereits in der Nachrichtenwarteschlange verarbeitet zu werden, richtig? –

+2

@IsmaelAbreu Ja, du hast Recht. Du hast einen Fehler in meinem Code gefunden. Wenn Sie zwei Popups in der Nachrichtenwarteschlange haben, werden zwei Nachfüllungen ausgelöst. Die erste Nachfüllung füllt den Stapel wieder auf. Beim zweiten Refill wird keine gültige Klausel gefunden, da der Stack nicht leer ist. Der Server stürzt daher mit 'FunctionClauseError' ab. Ich habe den Fehler behoben und die neue Klausel hinzugefügt, wenn die Nachfüllungen einen nicht leeren Stapel finden. Danke: D –

2

Warum müssen Sie GenServer.call machen?

def handle_call(:pop, state) do 
    new_state0 = if is_empty_queue(state) do 
    put_some_stuff_in_queue(state) 
    else 
    state 
    end 
    {val,new_state} = pop_something(new_state0) 

    {:reply, val, new_state} 
end 

oder

def handle_call(:pop, state) do 
    {val, new_state} = state 
        |> is_empty_queue 
        |> case do 
          true -> 
          put_some_stuff_in_queue(state) 
          false -> 
          state 
         end 
        |> pop_something 

    {:reply, val, new_state} 
end 

so Telefonieren ist tabu, sondern auch andere Funktionen aufrufen völlig machbar ist.

+0

Die grundlegende Unterscheidung ist, dass ich mit meiner ursprünglichen Implementierung die Warteschlange separat auffüllen kann, wenn ich muss. Es mag Zeiten geben, in denen ich das machen will, unabhängig davon, etwas daraus zu machen.Natürlich kann ich die refill_queue 'handle_call' in Ruhe lassen, was ich erwarte, aber ich frage mich nur, ob es eine idiomatische Elixier-Art gibt, mit dieser Art von Situation umzugehen. – Micah

Verwandte Themen