淺談 Ruby 的 Fiber(四)
在上週的文章我們注意到 Fiber 的使用並不是那麼容易的,因為我們需要自行管理每一個 Fiber 被恢復(#resume
)的時機,這週就繼續來挑戰吧!
思考
在上週我們已經知道需要將行為從原本會阻塞 I/O 的操作,轉換為非阻塞的操作。所以我們會進行以下的嘗試,來修正 Fiber 的運行。
- 使用
#accept_nonblock
來取得用戶端 - 使用
#read_nonblock
來讀取使用者的資料
嘗試
1server = TCPServer.new 3000
2
3fibers = []
4loop do
5 begin
6 client = server.accept_nonblock
7 client.puts 'Hello World'
8
9 fibers.each(&:resume)
10
11 fiber = Fiber.new do
12 buffer ||= ''
13 begin
14 buffer << client.read_nonblock(1024)
15 if buffer.include?("\n")
16 puts buffer
17 client.close
18 end
19 rescue IO::WaitReadable
20 puts 'RETRY'
21 Fiber.yield
22 retry
23 end
24 end
25
26 fiber.resume
27 fibers << fiber
28 rescue IO::WaitReadable
29 sleep 1
30 retry
31 end
32end
我們對原本的做了一些修改,把 #accept_nonblock
和 #read_nonblock
加入到了程式碼中,在這邊我們可以利用 retry
關鍵字觸發重試的行為,讓我們遇到 IO::WaitReadable
錯誤時,可以自動地重新開始。
不過,我們還是發現了一些問題不太正常。
- 依然要在第下一個連線開始後才會斷線
- 到第三次之後就不正常的斷線
分析
首先我們來分析一下為什麼還是無法在使用者輸入訊息後關閉連線。
- 等待連線
- 重試(…)
- 第一個連線連上
- 執行
fibers.each(&:resume)
動作 - 執行初次連線的 Fiber 生成和
#resume
動作 - 等待連線
- 重試(…)
透過上面的流程,我們會發現要觸發使用者輸入訊息後斷線的行為,會因為「沒有人連上」的重試行為,一直被卡在 #accept_nonblock
這個狀態上(因為他會產生 IO::WaitReadable
錯誤)
而第二個錯誤,則是在第三次連線後,會出現 dead fiber called (FiberError)
這個錯誤訊息,這是因為當我們用盡 Fiber.yield
次數後,這個 Fiber 就無法再繼續進行 #resume
否則就會發生這個錯誤。
解析
事實上,這兩個問題是一起發生的。假設我們先將 fibers.each(&:resume)
放到 #accept_nonblock
前面,那麼很快地就會將 dead fiber called
這個錯誤觸發,所以我們還需要搭配在關閉連線時,將目前所屬的 Fiber 從 fibers
移除掉,才能夠確保程式正常運行。
小結
目前已經逐漸抓到一些線索,可以讓我們針對 Fiber 進行應用,基本上這次的錯誤修正後就可以獲得跟我們預期中一樣運行的 TCP Server 了。
不過,如果想要將它改寫成一個簡易的聊天室,又要怎麼做?會碰到什麼問題呢?