Threads 和 asynchronous environments 最初有點 tricky 沒有好的 mental model 來組織 interaction 很容易陷入麻煩並以 unexpected results 結束 最重要的是 沒有正確的 tools 或 test patterns 測試 asynchronous code 可能很難
將 threads 視為人 和 shared objects 視為可以被擁有的 'things' 有助於組織 multithreaded system 的運作 在這一集 我們將通過一個例子學習關於測試 asynchronous Ruby 代碼的所有內容
如果你使用 Rails 或 Rack 或真正任何有 web browser front end 的 application 你就在 asynchronous environment 中 Rack #call 總是被異步調用 所以無論你知不知道 很有可能你已經在使用 multithreaded components
Testing: Trigger, collect and check
測試 asynchronous callback API 可以通過遵循三個步驟的 pattern 使其同步; trigger, collect, 和 check 想像每個 thread 作為一個獨立個體 和 objects 作為 好吧 可以被一個個體一次擁有的 'things'
我們將使用 Batman 和他的 7 套不同 suits 的例子 因為那是一個 practical example 並且我們可以理解知道所有 suits 是否都在 Alfred 那裡洗滌當你準備衝出去拯救城市時的重要性
Example: Laundry day at the batcave
例子是 Alfred 洗 Batman 的 suits SuitWashScheduler 是一個 scheduler 為每個 washing event invoke 一個 callback Scheduler 在開始後一秒開始以一秒間隔進行七次 callbacks Trigger 是創建 SuitWashScheduler
class SuitWashScheduler
def initialize(cnt)
Thread.new {
cnt.times {
sleep(1.0)
yield
}
}
end
end
Collecting
收集結果必須是 thread-safe 以避免 race conditions 跨越多個線程共享的任何對象必須受到保護 Protection 是一種跟踪對象 owner 的方式 只有 owner 可以進行更改或查看對象 一套 suit 只能在 Batman 手中用於戰鬥 或在 Alfred 那裡洗滌
為了保持友好 一個 thread (在隱喻中 Batman 或 Alfred) 只擁有所有權很短的時間然後放棄所有權 一個 Mutex 通常用於跟踪 owner SuitWashScheduler callback 將擁有 result counter 當 counter 被 increment 時 SuitWashScheduler 線程中運行的 callback 發出信號所有結果已收到 當 counter hit target
編寫例子從設置一些 globals 開始 在真實應用中 globals 將被 class 或 object attributes 替換
$main_thread = Thread.current
$mu = Mutex.new
$count = 0
$target = 7
Management and owners
$main_thread 和 $mu 用於管理線程和等待測試完成 而 $target 和 $count 跟踪測試結果 記住這是一個 trivial test 所以收集和檢查結果必須簡單
測試通過創建 SuitWashScheduler 的新實例啟動 給 initializer $target 迭代次數 在這種情況下 7 套需要洗滌 提供的塊將在 SuitWashScheduler 線程中運行 對於每次迭代 $count 被 increment 和 printed
展望未來 我們意識到 main, test thread 也將檢查 $count 這意味著它也將需要 $count 的所有權 所以需要一種獲取 $count 所有權的方法 $mu Mutex 實例是 ownership token 在傳遞給 SuitWashScheduler.new 的塊中 一個 $mu.synchronize 塊獲取所有權足夠長的時間以設置 $count 和檢查結果 更多關於結果檢查稍後
SuitWashScheduler.new($target) {
$mu.synchronize {
$count += 1
puts $count
$main_thread.wakeup if $target <= $count
}
}
Check: are all suits done?
回到 main thread 我們需要等待測試完成 Batman 需要等待所有 7 套完成 有兩個條件要檢查; 測試按預期更新 $count 或 Batman 厭倦等待測試完成並 timeout 在檢查 $count 看它是否達到 $target 之前 需要 $count 的所有權 就像在 SuitWashScheduler 的塊中 一樣調用 $mu.synchronize
但那不可能對 如果我們 lock up main thread SuitWashScheduler 線程如何能改變 $count? 幸運的是對我們來說有一個 neat trick 處理這個 Mutex 類有一個 #sleep 方法 放棄所有權並等待直到 timeout 或被喚醒 一旦被喚醒 通過 timeout 或對 main thread 的 #wakeup 調用 $mu 嘗試再次獲取所有權然後繼續 一旦獲得所有權 結果可以被檢查並決定測試的 pass 或 fail 狀態
$mu.synchronize {
$mu.sleep($target + 1)
if $target != $count
puts 'FAILED'
else
puts 'Passed! All suits are washed and clean'
end
}
如果你想深入了解 你可以通過嘗試創建多個 schedulers 來使例子更有趣 看看 Mutex 如何保持 $count 變化不碰撞 就像 Batman 送一些 suits 給 Alfred 洗 另一些送去乾洗 確保改變邏輯使 $target 檢查是所有預期 yields 的總和
Roundup
使用正確的 mental model 使 threads 和 asynchronous environment 變容易 在這篇文章中 我們用人作為 threads 的隱喻 和物理對象 (suits) 作為 shared objects 的隱喻 只能被一個 thread 或人一次擁有 我們這樣思考以抽象使它更容易理解和記住
我們希望例子會讓你記住 async 的機制 但我們希望 Batman 衝出去而所有 suits 都在洗的形象不會在你腦海停留太久
P.S. 如果你厭倦了部落格上所有的 batman metaphors 讓我們知道