iOS開發(fā)文集iOS 10 Day By Day: Thread Sanitizer線程檢查工具
通常,這些是由于多個線程同時訪問相同的一些內(nèi)存而造成的。我猜想,線程問題是許多開發(fā)人員做噩夢的原因。他們是出了名的難以追蹤,錯誤只發(fā)生在特定條件下:所以確定問題的根源是非常復雜的。
通常導致線程問題的原因是所謂的“競爭條件”。我們不會去關注太多的細節(jié),像是這意味著什么,而是從谷歌引用ThreadSanitizer手冊:
數(shù)據(jù)競爭發(fā)生在當兩個線程同時訪問同一變量,并且至少有一個訪問是編寫狀態(tài)時。
這些用來追蹤的是一個絕對的噩夢,但值得慶幸的是Xcode附帶一個新的調(diào)試工具叫做Thread Sanitizer,甚至可以在你注意到他們之前幫助識別這些問題。
The Project
我們將創(chuàng)建一個簡單的應用程序,使我們能夠存款和取款100美元面額。像往常一樣,項目的完成版本已在GitHub上(為了方便各位讀者,小編已經(jīng)為大家整理了,請點擊這里下載)。
The Account
我們的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 } // Simulate processing of fraud checks sleep(2) self.balance = newBalance completed() } func deposit(amount: Int, completed: () -> ()) { let newBalance = self.balance + amount self.balance = newBalance completed() } }
它包含幾個方法使我們能夠取款和存款到我們的賬戶。存款和取款金額是硬編碼$100。
deposit方法已經(jīng)幾乎立即執(zhí)行,然而,withdraw還需要一段時間才能完成。我們會說這是因為我們需要為取款執(zhí)行一些欺詐檢查,但實際上我們只發(fā)送當前線程睡眠2秒。這將給我們后面使用一些多線程提供借口。
唯一需要注意的另一件事是完成模塊,這是當存款和取款都成功完成時才執(zhí)行。
視圖控制器
我們的視圖控制器由兩個按鈕——存款和取款,以及一個顯示當前余額的標簽組成。故事板的布局:
為連接我們的UI元素,我們有一個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)" } }
讓我們給它一個旋轉(zhuǎn):
嗯……當我們試著取回錢時有點慢!這是由于我們Account 的withdraw方法及其嚴格的“欺詐檢查”,導致主線程阻塞,直到該方法已經(jīng)完成。我們希望用戶能夠以最小的延遲反復點擊“Deposit”和“Withdraw”。
救援調(diào)度隊列
如果我們可以從主線程刪除阻塞的withdraw方法,這就太棒了。我們將使用新“Swiftified”中央調(diào)度庫:
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 } // Simulate processing of fraud checks sleep(2) self.balance = newBalance DispatchQueue.main.async { onSuccess() } } }
讓我們再次運行它:
等一等!我們的錢哪里去了?我們存入100美元,取回了100美元然后,剩下0,盡管開始時是100美元!
我們有信心我們的方法按預期的運行(因為他們是單元測試),它看起來就像我們的withdraw任務調(diào)度到背景隊列引發(fā)了一個問題。
Thread Sanitizer線程檢查工具來拯救我們的理智!
打開檢查工具非常簡單,只需將你的目標的計劃設置和在Diagnostics標簽中檢查Thread Sanitizer箱。我們可以選擇在遇到的問題上暫停,這使得它能夠容易地在個案基礎上評估每一個問題。我們會這樣。
由于線程檢查工具只在運行時起作用,我們需要重新編譯和重新運行應用程序。讓我們開始吧。
在WWDC上,蘋果建議在你所有的單元測試開啟線程檢查工具。檢查工具在運行時操作,如果代碼執(zhí)行,只能夠確定數(shù)據(jù)競爭。如果你的代碼完全得以單元測試,那么你可能會發(fā)現(xiàn)線程檢查工具發(fā)現(xiàn)了大多數(shù)問題,如果不是全部測試,發(fā)現(xiàn)的是你的項目的競態(tài)條件 (你會發(fā)現(xiàn)我們博客的iOS 9 Day by Day中一個有用的閱讀,Xcode 7的代碼覆蓋工具)。
其他值得注意的是,它只能運行在語言版本3編寫的Swift代碼上(Objective-C也可兼容),并且只能使用64位模擬器運行。
當我們重復我們之前取款的過程,然后立即存款,線程檢查工具會暫停我們的應用程序的執(zhí)行,因為它發(fā)現(xiàn)了競態(tài)條件。這給了我們一個很好的沖突訪問發(fā)生的地方的堆棧跟蹤。
它還將結(jié)果輸出到控制臺,所以你沒有必要從Xcode運行檢查工具。
通過堆棧跟蹤和提供的信息,線程分析儀有助于表明,當訪問Account.balance屬性時在我們的Account.deposit和Account.withdraw方法中有一個數(shù)據(jù)競爭。哦,看來我們需要在withdraw和deposit方法中使用相同的串行調(diào)度隊列:
我們將修改我們的Account類來使用共享隊列:
class Account { var balance: Int = 0 private let queue = DispatchQueue(label: "com.shinobicontrols.balance-moderator") func withdraw(amount: Int, onSuccess: () -> ()) { queue.async { // Same as earlier... } } func deposit(amount: Int, onSuccess: () -> ()) { queue.async { let newBalance = self.balance + amount self.balance = newBalance DispatchQueue.main.async { onSuccess() } } } }
再次運行應用程序顯示了我們?nèi)匀挥袛?shù)據(jù)競爭,但是它不再是在我們的Account類中,而是由于我們的ViewController從主線程訪問balance。
我們可以通過轉(zhuǎn)換到一個只有訪問Account的私有變量來保護我們的balance屬性,而不是用我們的隊列返回balance。
private var _balance: Int = 0 var balance: Int { return queue.sync { return _balance } }
我們需要轉(zhuǎn)換任何書面到平衡變量以使用私有_balance屬性。
現(xiàn)在當我們運行我們的應用程序,我們應該能夠多次點擊“withdraw”和“deposit”而無需令人不安的線程檢查工具。太好了,我們剛剛使用這個新工具來修正了我們的錯誤代碼。
進一步的閱讀
雖然看起來似乎不像起初那樣,線程檢查工具可能會成為開發(fā)人員工具箱中一個非常重要的iOS工具。它發(fā)現(xiàn)數(shù)據(jù)競爭的能力,即使在程序的運行期間沒有發(fā)生,也可能會拯救無數(shù)小時調(diào)試斷斷續(xù)續(xù)的線程問題的時間。
像往常一樣,蘋果的WWDC大會很豐富,值得一看。sanitizer是Clang編譯器的一部分,在LLVM網(wǎng)站上可以找到更詳細的信息,在谷歌建立了sanitizer的團隊有許多有趣的wiki頁面,其中包括了算法用于檢測線程問題的高層次的演練。
我們使用Swift 3中提供給我們的一個小的新面貌GCD。蘋果也在“Concurrent Programming With GCD in Swift 3”談話中談到了這個,你可能會發(fā)現(xiàn)它的用處。此外,Roy Marmelstein寫了一篇很好且簡潔的帖子闡述這一變化。
本文翻譯自:iOS 10 Day by Day: Thread Sanitizer