本文介紹了 Xcode 8 的新出的多線程調試工具 Thread Sanitizer,能夠在 app 運行時發現線程競態。做者用經典的銀行存取錢爲例子,示例使用這個工具發現線程不安全的問題。html
《iOS 10 day by day》是 shinobicontrols 公司編寫的系列博客,介紹開發者須要瞭解的 iOS 10 新特性,每週更新。本系列翻譯(文集地址)已取得官方受權。倉薯翻譯,歡迎指正:)ios
Shinobicontrols 爲 iOS 和 Android 開發者提供高性能、響應式的 UI 控件 SDK,尤爲是圖表方面的控件。 官網 : shinobicontrols.com twitter : @shinobicontrolsgit
想一想一下,你的 app 已經近乎大功告成:它通過精良的打磨,單元測試全覆蓋。只剩下一個問題:有一個很嚴重的 bug,可是是偶發的,你已經花了好幾個小時嘗試修復它卻一無所得。問題到底出在哪裏呀?程序員
這種狀況常常是多個線程訪問同一塊內存形成的。我能夠大膽猜想,多線程的 bug 是許多程序員的夢魘。這類 bug 很是難定位,並且只有特定條件下才能重現:因此找出問題的緣由確實困難重重。github
而問題的緣由經常是所謂的『線程競態』。對這個名詞咱們再也不多費筆墨去解釋了,如下摘自 Google 的 ThreadSanitizer 文檔:算法
兩個線程同時訪問同一個變量,並且其中至少一個線程要作的是寫操做,這種狀況就叫競態。編程
調試競態問題曾經讓程序員們大爲頭疼;不過值得慶幸的是,Xcode 發佈了一個新的線程調試工具叫作 Thread Sanitizer 能夠檢測出這類問題,甚至比你發現得還早。swift
咱們作了一個簡單的應用,能讓用戶存錢、取錢,每次 $100。跟以前同樣,最終版的工程放在 Github 上了。xcode
銀行帳戶的數據模型很簡單,名爲Account
:安全
import Foundation class Account { var balance: Int = 0 func withdraw(amount: Int, completed: () -> ()) { let newBalance = self.balance - amount if newBalance < 0 { print("You don't have enough money to withdraw \(amount)") return } // 模仿銀行的防僞驗證過程 sleep(2) self.balance = newBalance completed() } func deposit(amount: Int, completed: () -> ()) { let newBalance = self.balance + amount self.balance = newBalance completed() } }
裏面只包含了這麼幾個方法,能讓咱們給帳戶存錢、取錢。存取的金額寫死爲 $100。
其中,deposit
方法是當即返回的,而withdraw
方法要花一點時間才能執行完。咱們名義上說是由於銀行要執行防僞驗證,背後其實就是讓線程 sleep 了 2 秒。這在後面能給咱們一個使用多線程的藉口。
另一點要注意的是 completed block,在存取成功以後執行。
View Controller 裏有兩個 button ——一個存錢、一個取錢——還有一個 label,顯示當前帳戶餘額。Storyboard 中的佈局是這樣的:
從 Storyboard 中引出顯示餘額 label 的 IBOutlet,再寫幾個方法更新餘額的顯示:
import UIKit class ViewController: UIViewController { @IBOutlet var balanceLabel: UILabel! let account = Account() override func viewDidLoad() { super.viewDidLoad() updateBalanceLabel() } @IBAction func withdraw(_ sender: UIButton) { self.account.withdraw(amount: 100, onSuccess: updateBalanceLabel) } @IBAction func deposit(_ sender: UIButton) { self.account.deposit(amount: 100, onSuccess: updateBalanceLabel) } func updateBalanceLabel() { balanceLabel.text = "Balance: $\(account.balance)" } }
來試一下吧:
嗯……取錢的過程有點慢呀。這是由於咱們所寫的withdraw
方法裏有嚴格的『防僞驗證』機制,在方法結束前會一直 block 主線程。而咱們但願的是用戶能快速重複存錢、取錢,把延遲降到最低。
若是要是能把withdraw
方法從主線程移出來,就解決這問題了。咱們能夠用上新出的『Swift 化』的 GCD 庫:
func withdraw(amount: Int, onSuccess: () -> ()) { DispatchQueue(label: "com.shinobicontrols.balance-moderator").async { let newBalance = self.balance - amount if newBalance < 0 { print("You don't have enough money to withdraw \(amount)") return } // 模仿銀行的防僞驗證過程 sleep(2) self.balance = newBalance DispatchQueue.main.async { onSuccess() } } }
再跑一次:
等一下,咱們的錢呢?一開始帳戶餘額是 $100,咱們先取了 $100,而後存了 $100,怎麼帳戶餘額只剩下 0 了呢?
存取方法確定是沒問題的(剛纔都分別測過了),看起來問題就出在把 withdraw
的任務放到單獨線程這一步。
開啓 Thread Sanitizer 很簡單,只需點擊 target 的 Edit Scheme...,而後在 Diagnostics
tab 下勾選 Thread Sanitizer
。能夠選擇 Pause on issues,這樣比較方便一步步調試問題。咱們把它勾上。
由於 thread sanitizer 只在運行時工做,咱們須要把工程從新編譯、從新跑一下。來試試吧。
在 WWDC 演講中,蘋果推薦在全部的單元測試裏都打開 thread sanitizer。Sanitizer 只在運行時有效,並且必需要代碼運行到那兒才能檢測出線程競態。若是你的代碼單元測試覆蓋率很高,那麼 Thread Sanitizer 能找出工程裏絕大部分的線程競態(能夠參考下咱們在 iOS 9 Day by Day 裏寫過的 Xcode 7 的測試覆蓋工具)。
.
還要注意的一點是,對於 Swift 這個工具只對 Swift 3 的代碼有效(Objective-C 也兼容),並且只能用 64 位的模擬器來跑。
如今咱們再把以前的操做重複一遍,先取錢,再立刻存錢。這時候 thread sanitizer 把 app 暫停了,由於它發現了線程競態。它清晰地展示出了衝突發生時的調用棧。
並且,它在控制檯裏打印出了相關信息。
經過調用棧和打印出的信息,Thread Analyzer 給力地幫咱們定位了問題所在: Account.deposit
方法與 Account.widthdraw
方法會訪問同一個屬性 Account.balance
,從而出現了競態。哎呀,看樣子咱們應該把存錢和取錢放在同一個線程裏進行。
咱們修改一下 Account
類的代碼,用一個公共的 queue:
class Account { var balance: Int = 0 private let queue = DispatchQueue(label: "com.shinobicontrols.balance-moderator") func withdraw(amount: Int, onSuccess: () -> ()) { queue.async { // 跟以前同樣... } } func deposit(amount: Int, onSuccess: () -> ()) { queue.async { let newBalance = self.balance + amount self.balance = newBalance DispatchQueue.main.async { onSuccess() } } } }
再跑一遍代碼,發現仍是有競態;只不過此次不是在 Account
類裏,而是由 ViewController
類在主線程訪問 balance
形成的。
爲解決這個問題,咱們能夠把 balance
屬性改爲 private 保護起來,只能在 Account
類內部訪問它,而後改用 queue 來返回結果。
private var _balance: Int = 0 var balance: Int { return queue.sync { return _balance } }
以前全部對 balance
屬性的寫操做都要改爲私有的 _balance
。
如今再跑一遍,再怎麼重複點擊 "withdraw" 和 "deposit" 都不會驚動 Thread Sanitizer 了。太棒啦——咱們用這個工具修好了多線程的 bug。
儘管看着不起眼,Thread Sanitizer 仍是頗有可能會成爲 iOS 開發者的一個重要工具。它能在程序運行沒出錯的狀況下就找到線程競態,能夠爲你省下大把時間 debug 間歇出現的多線程問題。
一如既往,蘋果的 WWDC 演講 信息量很大,值得一看。Sanitizer 是 Clang 編譯器的一部分,更詳細的信息能夠參考 LLVM 的官網,還有 Google 開發 sanitizer 的團隊編寫了許多有趣的 wiki,其中包括對檢測多線程問題算法的簡單介紹。
咱們用到了一點 Swift 3 新出的 GCD 語法。Apple 在Swift 3 的 GCD 併發編程的演講中對此有所介紹,能夠看一看。另外,Roy Marmelstein 也有一篇短小精悍的博客介紹其中的變化。
若是有任何問題和評論,咱們都很歡迎你的反饋。能夠發我 tweet @sam_burnstone,也能夠關注 @shinobicontrols 關注最新動態以及 iOS 10 Day by Day 系列的更新。感謝閱讀!
原文地址:iOS 10 Day by Day :: Day 2 :: Thread Sanitizer
原做者:Sam Burnstone @sam_burnstone
ShinobiControls 官網:ShinobiControls.com twitter : @shinobicontrols
譯者:戴倉薯