2016-12-13 13 views
7

Tôi biết rằng có quá trình GenServer không thể tự gọi được vì bạn về cơ bản bị bế tắc. Nhưng, tôi tò mò nếu có một cách ưa thích để làm điều này.Cách thích hợp để cấu trúc các cuộc gọi GenServer tới chính mình

Giả sử trường hợp sau: Tôi có hàng đợi rằng tôi đang popping mọi thứ. Nếu hàng đợi trống, tôi muốn nạp nó lại. Tôi có thể cấu trúc nó như vậy:

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 

Vấn đề lớn ở đây là điều này sẽ bế tắc khi chúng tôi cố nạp lại hàng đợi. Một giải pháp mà tôi đã sử dụng trong quá khứ là sử dụng cast nhiều hơn để nó không bế tắc. Như vậy (thay đổi call để cast cho refill)

def handle_cast(:refill_queue, state) do 

Nhưng trong trường hợp này, tôi nghĩ rằng nó sẽ không làm việc, kể từ khi các diễn viên async để nạp tiền hàng đợi có thể trở lại trong trường hợp pop trước khi thực sự làm đầy ý nghĩa hàng đợi Tôi sẽ cố gắng bật ra một hàng trống.

Dù sao, câu hỏi cốt lõi là: Cách tốt nhất để xử lý vấn đề này là gì? Tôi cho rằng câu trả lời là chỉ gọi put_some_stuff_in_queue trực tiếp trong cuộc gọi pop, nhưng tôi muốn kiểm tra. Nói cách khác, có vẻ như việc phải làm là làm cho handle_callhandle_cast càng đơn giản càng tốt và về cơ bản chỉ là trình bao bọc cho các chức năng khác nơi công việc thực sự xảy ra. Sau đó, tạo nhiều chức năng handle_* khi bạn cần bao gồm tất cả các trường hợp có thể xảy ra, thay vì có handle_call(:foo) lần lượt gọi handle_call(:bar).

+2

Tôi sẽ làm cho 'refill_queue' trở thành một hàm đơn giản và gọi nó từ' handle_call (: pop) 'nếu bạn cần nó đồng bộ. Nếu không, bạn có một số tùy chọn để xử lý async (gửi một thông điệp khác đến 'self', có một quá trình xử lý nạp lại, vv) –

+3

Bạn nên xem [' GenStage'] (http://elixir-lang.org/blog/2016/07/14/thông báo-genstage /), có vẻ như nó cung cấp chức năng bạn đang tìm kiếm. – mudasobwa

Trả lời

5

Có chức năng trong GenServer mô-đun được gọi là reply/2. Đối số thứ hai của cuộc gọi lại handle_call/3 là kết nối với máy khách. Bạn có thể tạo một quy trình mới để xử lý kết nối và trả về {:noreply, state} trong mệnh đề gọi lại. Sử dụng ví dụ của bạn:

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 

Và bạn sẽ nhận được như sau:

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}] 

Vì vậy, nó là trong thực tế có thể cho một GenServer để làm yêu cầu riêng của mình. Bạn chỉ cần biết làm thế nào.

Tôi hy vọng điều này sẽ hữu ích.

+0

Vâng, tôi nghĩ rằng điều này là khá nhiều chính xác những gì tôi đã tự hỏi. Tôi đã tự hỏi lập luận 'từ' nào đã được sử dụng, và bây giờ nó có ý nghĩa hơn. – Micah

+3

Tôi không biết bạn có thể trả lời sau, trong khi bạn xử lý các tin nhắn khác tuyệt vời để biết. NHƯNG! Điều này sẽ không làm cho nhiều cuộc gọi pop có thể gọi nạp tiền cùng một lúc không? Tôi có nghĩa là, nếu nạp tiền mất 10ms và chúng tôi có một cuộc gọi pop mỗi 3ms chúng tôi sẽ nhận được 2 pops (mỗi kích hoạt thêm một nạp tiền) trong khi đầu tiên là làm nạp tiền. Bởi vì việc nạp tiền đó sẽ đi vào hàng đợi tin nhắn, để được xử lý sau khi các pops đã có trong hàng đợi tin nhắn, phải không? –

+2

@IsmaelAbreu Vâng, bạn nói đúng. Bạn đã tìm thấy một lỗi trong mã của tôi. Nếu bạn có một hai pops trong hàng đợi tin nhắn này sẽ kích hoạt hai nạp. Việc nạp tiền đầu tiên sẽ đổ đầy ngăn xếp. Lần nạp thứ hai sẽ không tìm thấy mệnh đề hợp lệ vì ngăn xếp không rỗng, do đó máy chủ sẽ gặp sự cố với 'FunctionClauseError'. Tôi đã sửa lỗi và thêm mệnh đề mới khi nạp lại tìm một ngăn xếp không trống. Cảm ơn bạn: D –

2

tại sao bạn cần tạo GenServer.call?

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 

hoặc

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 

để thực hiện cuộc gọi là không-no nhưng kêu gọi các chức năng khác là hoàn toàn khả thi.

+0

Sự khác biệt cơ bản là với việc triển khai ban đầu của tôi, tôi có thể nạp lại hàng đợi một cách riêng biệt nếu tôi cần. Có thể có những lúc tôi muốn làm điều đó một cách độc lập với việc popping cái gì đó.Tất nhiên, tôi có thể để lại refill_queue 'handle_call' một mình, đó là loại những gì tôi mong đợi, nhưng tôi chỉ tự hỏi nếu có một cách Elixir thành ngữ để xử lý loại tình huống này. – Micah

Các vấn đề liên quan