淺談 Ruby 的 Fiber(三)
延續上一篇文章的實作,我們已經有一個簡易的 Thread 版本 TCP Socket 伺服器可以運作,那麼該怎麼用 Fiber 修改呢?
思考
在我們使用 Thread 的時候,應該要先執行哪個 Thread 中的任務會由作業系統或者語言本身底層的實作來協助我們處理,但是 Fiber 目前只能用來在不同的程式碼片段中切換,所以我們就需要自己管理應該要切換到哪一個片段。
所以我們要先定義一個情境:
當 Blocking I/O 發生的時候,會發生什麼事情?
1# Server 嘗試讀取 Client 的資料
2resp = client.gets
3# Blocking I/O 發生
4# ...
5# ...
6# 使用者輸入 HELLO
7# Blocking I/O 結束
8puts resp
9# => HELLO
其實就是當我們嘗試做 Read(讀取)跟 Write(寫入)的時候,暫時無法操作的狀態。
Ex. 想讀取資料卻無法讀取
所以,根據上一篇文章的案例,當我們嘗試 #gets
卻沒有得到使用者的回應時,就應該先透過 Fiber.yield
把執行權限釋放出來。
嘗試
因為邏輯上跟以往我們習慣的方式不太一樣,所以我們需要進行多次的嘗試,讓 Fiber 可以像我們預期的一樣運作。
1server = TCPServer.new 3000
2
3fibers = []
4loop do
5 client = server.accept
6 client.puts 'Hello World'
7
8 fibers.each(&:resume)
9
10 fiber = Fiber.new do
11 Fiber.yield
12 puts client.gets
13 client.close
14 end
15
16 fiber.resume
17 fibers << fiber
18end
這樣看起來好像會運作,不過當我們嘗試執行的時候,卻發現有一些奇怪的地方。
- 輸入訊息後沒有馬上斷線
- 第二個人連上後才會斷線
分析
當第一個人連上後,會發生以下事情:
- 先把
fibers
裡面存在的 Fiber 執行#resume
一次 - 對這次連上的使用者產生一個
Fiber
- 先執行一次(抵達第一次的
Fiber.yield
)被暫停 - 將目前的 Fiber 物件儲存在
fibers
中 - 重新呼叫
server.accept
-> Blocking I/O
第二個人連上後,會發生以下的事情
- 先把
fibers
裡面存在的 Fiber 執行#resume
一次 - 第一個連線的使用者執行
#gets
行為 -> Blocking I/O
到目前為止已經被 Blocking I/O 堵住兩次,還比原本沒有 Fiber 的版本更難懂,那麼問題出在哪裡呢?
解析
首先,我們希望程式上遇到 Blocking I/O 時先不要卡住我們,所以我們需要將 server.accept
和 client.gets
這兩個行為調整成 Nonblocking I/O 的使用方式。
也就是當遇到 Blocking I/O 的時候我們希望直接回傳「沒有資料」之類的訊息給我們,而不是直接停住。
1loop do
2 puts 'LOOPING'
3 begin
4 client = server.accept_nonblock
5 # 有人連上了!
6 rescue IO::WaitReadable
7 # 目前都沒有人連線!
8 end
9end
以 #accept_nonoblock
這個用法作為例子,我們會發現,假設我們做 puts 'LOOPING'
這段程式碼,如果是 #accept
的時候,需要有人連上才會顯示訊息,但是在 #accept_nonblock
的時候,則會不斷出現。
同時,我們也會得到一個叫做 IO::WaitReadable
的錯誤,告訴我們現在雖然讀取了,但是實際上是無法讀取任何東西的,需要等待有東西可以讀取。
透過這樣的機制,我們就可以在碰到這個錯誤時進行 Fiber.yield
先讓他暫停,等到我們確認他可以讀取後,再繼續執行。
小結
這篇文章我們發現如果使用 Thread 的話,基本上是由作業系統去管理碰到 Blocking I/O 時該怎麼做,但是如果是 Fiber 的話,我們就得完全靠 Ruby 來解決。
同時也會遇到某些情境無法使用的狀況,像是 net/http
這個標準函式庫,並沒有提供 Nonblocking I/O 的方法,我們就無法透過在 Blocking I/O 狀態下先讓他暫停,並且先切換到其他工作上。