蒼時弦也
蒼時弦也
資深軟體工程師
發表於

淺談 Ruby 的 Fiber(七)

上週我們開始重構 Fiber 的結構,透過一個統一的 Selector 物件來選取這個「當下」可以進行 I/O 操作的物件。

不過,我們原本預期是因為使用 rescue 來捕捉錯誤控制流程才讓他運行不正常,經過一週的思考後,卻發現事情跟預想的不太一樣。

調整

我們先快速的使用 exception: false 的模式調整程式,會變成類似這樣的結構。

 1Fiber.new do
 2  loop do
 3    client = server.accept_nonblock(exception: false)
 4    selector.register(server) if client == :wait_readable
 5    next if client == :wait_readable
 6
 7    Fiber.new do
 8      buffer ||= ''
 9      loop do
10        read = client.read_nonblock(1024, exception: false)
11        selector.register(client) if read == :wait_readable
12        next if read == :wait_readable
13        buffer << read
14        puts buffer if buffer.include?("\n")
15      end
16    end.resume
17  end
18end.resume

然而,現實上並沒有實際的改善卡住的問題,和我們原本預測只要釐清流程就會正常的方向不太一樣。

反思

目前我們已經透過 Selector 物件來管理所有的 I/O 行為,現在程式碼只有一個地方能夠對 I/O 造成阻塞,那就是 IO.select 這個方法,會將最後一次我們紀錄「等待中」的物件傳入,等待作業系統提供可繼續 I/O 行為的通知。

1def resume
2    puts @fibers.keys
3    readable, = IO.select(@fibers.keys)
4    readable.each do |io|
5      @fibers[io].resume
6      @fibers.delete(io)
7    end
8  end

我們嘗試將放在 @fibers 中的物件列出來,就會發現實際上一直都只有一個。原本預期會存在的 TCPServer 會在第一個使用者連上後就被移除。

而我們預期的應該要是在這一個 I/O 操作完畢後,將他移除。

修改

我們對 Selector 做出一些修改,變成類似像這樣的結構。

 1class Selector
 2  def initialize
 3    @fibers = {}
 4  end
 5
 6  def register(io)
 7    @fibers[io] = Fiber.current
 8    Fiber.yield
 9    @fibers.delete(io)
10  end
11
12  def resume
13    readable, = IO.select(@fibers.keys)
14    readable.each do |io|
15      @fibers[io].resume
16    end
17  end
18end

如此一來,所有使用者都能夠正常的連上並且寫入資料到伺服器上。

小結

不過,現實並沒有我們預期的那麼容易,因為我們在其中一個使用者離開後,發現 @fibers.delete(io) 並沒有被執行,在下一篇文章我們要來尋找原因並且將它解決。

在這將近一個半月的文章,我們會發現使用 Fiber 並沒有預期中的容易,而且似乎並沒有想像中的實用,但是透過這樣的方式,也可以去思考為什麼 Ruby 中要存在 Fiber 以及到底是用來處理怎麼樣的特殊狀況,才會需要用到。