iOS開發(fā)文集iOS 10 Day By Day: Thread Sanitizer線程檢查工具
通常,這些是由于多個(gè)線程同時(shí)訪問相同的一些內(nèi)存而造成的。我猜想,線程問題是許多開發(fā)人員做噩夢(mèng)的原因。他們是出了名的難以追蹤,錯(cuò)誤只發(fā)生在特定條件下:所以確定問題的根源是非常復(fù)雜的。
通常導(dǎo)致線程問題的原因是所謂的“競(jìng)爭(zhēng)條件”。我們不會(huì)去關(guān)注太多的細(xì)節(jié),像是這意味著什么,而是從谷歌引用ThreadSanitizer手冊(cè):
數(shù)據(jù)競(jìng)爭(zhēng)發(fā)生在當(dāng)兩個(gè)線程同時(shí)訪問同一變量,并且至少有一個(gè)訪問是編寫狀態(tài)時(shí)。
這些用來追蹤的是一個(gè)絕對(duì)的噩夢(mèng),但值得慶幸的是Xcode附帶一個(gè)新的調(diào)試工具叫做Thread Sanitizer,甚至可以在你注意到他們之前幫助識(shí)別這些問題。
The Project
我們將創(chuàng)建一個(gè)簡(jiǎn)單的應(yīng)用程序,使我們能夠存款和取款100美元面額。像往常一樣,項(xiàng)目的完成版本已在GitHub上(為了方便各位讀者,小編已經(jīng)為大家整理了,請(qǐng)點(diǎn)擊這里下載)。
The Account
我們的Account模式非常簡(jiǎn)單:
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() } }
它包含幾個(gè)方法使我們能夠取款和存款到我們的賬戶。存款和取款金額是硬編碼$100。
deposit方法已經(jīng)幾乎立即執(zhí)行,然而,withdraw還需要一段時(shí)間才能完成。我們會(huì)說這是因?yàn)槲覀冃枰獮槿】顖?zhí)行一些欺詐檢查,但實(shí)際上我們只發(fā)送當(dāng)前線程睡眠2秒。這將給我們后面使用一些多線程提供借口。
唯一需要注意的另一件事是完成模塊,這是當(dāng)存款和取款都成功完成時(shí)才執(zhí)行。
視圖控制器
我們的視圖控制器由兩個(gè)按鈕——存款和取款,以及一個(gè)顯示當(dāng)前余額的標(biāo)簽組成。故事板的布局:

為連接我們的UI元素,我們有一個(gè)IBOutlet,引用平衡標(biāo)簽和以用戶當(dāng)前的平衡更新標(biāo)簽的方法。
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)" } }
讓我們給它一個(gè)旋轉(zhuǎn):

嗯……當(dāng)我們?cè)囍』劐X時(shí)有點(diǎn)慢!這是由于我們Account 的withdraw方法及其嚴(yán)格的“欺詐檢查”,導(dǎo)致主線程阻塞,直到該方法已經(jīng)完成。我們希望用戶能夠以最小的延遲反復(fù)點(diǎn)擊“Deposit”和“Withdraw”。
救援調(diào)度隊(duì)列
如果我們可以從主線程刪除阻塞的withdraw方法,這就太棒了。我們將使用新“Swiftified”中央調(diào)度庫(kù):
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() } } }
讓我們?cè)俅芜\(yùn)行它:

等一等!我們的錢哪里去了?我們存入100美元,取回了100美元然后,剩下0,盡管開始時(shí)是100美元!
我們有信心我們的方法按預(yù)期的運(yùn)行(因?yàn)樗麄兪菃卧獪y(cè)試),它看起來就像我們的withdraw任務(wù)調(diào)度到背景隊(duì)列引發(fā)了一個(gè)問題。
Thread Sanitizer線程檢查工具來拯救我們的理智!
打開檢查工具非常簡(jiǎn)單,只需將你的目標(biāo)的計(jì)劃設(shè)置和在Diagnostics標(biāo)簽中檢查Thread Sanitizer箱。我們可以選擇在遇到的問題上暫停,這使得它能夠容易地在個(gè)案基礎(chǔ)上評(píng)估每一個(gè)問題。我們會(huì)這樣。


由于線程檢查工具只在運(yùn)行時(shí)起作用,我們需要重新編譯和重新運(yùn)行應(yīng)用程序。讓我們開始吧。
在WWDC上,蘋果建議在你所有的單元測(cè)試開啟線程檢查工具。檢查工具在運(yùn)行時(shí)操作,如果代碼執(zhí)行,只能夠確定數(shù)據(jù)競(jìng)爭(zhēng)。如果你的代碼完全得以單元測(cè)試,那么你可能會(huì)發(fā)現(xiàn)線程檢查工具發(fā)現(xiàn)了大多數(shù)問題,如果不是全部測(cè)試,發(fā)現(xiàn)的是你的項(xiàng)目的競(jìng)態(tài)條件 (你會(huì)發(fā)現(xiàn)我們博客的iOS 9 Day by Day中一個(gè)有用的閱讀,Xcode 7的代碼覆蓋工具)。
其他值得注意的是,它只能運(yùn)行在語(yǔ)言版本3編寫的Swift代碼上(Objective-C也可兼容),并且只能使用64位模擬器運(yùn)行。
當(dāng)我們重復(fù)我們之前取款的過程,然后立即存款,線程檢查工具會(huì)暫停我們的應(yīng)用程序的執(zhí)行,因?yàn)樗l(fā)現(xiàn)了競(jìng)態(tài)條件。這給了我們一個(gè)很好的沖突訪問發(fā)生的地方的堆棧跟蹤。

它還將結(jié)果輸出到控制臺(tái),所以你沒有必要從Xcode運(yùn)行檢查工具。
通過堆棧跟蹤和提供的信息,線程分析儀有助于表明,當(dāng)訪問Account.balance屬性時(shí)在我們的Account.deposit和Account.withdraw方法中有一個(gè)數(shù)據(jù)競(jìng)爭(zhēng)。哦,看來我們需要在withdraw和deposit方法中使用相同的串行調(diào)度隊(duì)列:
我們將修改我們的Account類來使用共享隊(duì)列:
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() } } } }
再次運(yùn)行應(yīng)用程序顯示了我們?nèi)匀挥袛?shù)據(jù)競(jìng)爭(zhēng),但是它不再是在我們的Account類中,而是由于我們的ViewController從主線程訪問balance。

我們可以通過轉(zhuǎn)換到一個(gè)只有訪問Account的私有變量來保護(hù)我們的balance屬性,而不是用我們的隊(duì)列返回balance。
private var _balance: Int = 0 var balance: Int { return queue.sync { return _balance } }
我們需要轉(zhuǎn)換任何書面到平衡變量以使用私有_balance屬性。
現(xiàn)在當(dāng)我們運(yùn)行我們的應(yīng)用程序,我們應(yīng)該能夠多次點(diǎn)擊“withdraw”和“deposit”而無需令人不安的線程檢查工具。太好了,我們剛剛使用這個(gè)新工具來修正了我們的錯(cuò)誤代碼。
進(jìn)一步的閱讀
雖然看起來似乎不像起初那樣,線程檢查工具可能會(huì)成為開發(fā)人員工具箱中一個(gè)非常重要的iOS工具。它發(fā)現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)的能力,即使在程序的運(yùn)行期間沒有發(fā)生,也可能會(huì)拯救無數(shù)小時(shí)調(diào)試斷斷續(xù)續(xù)的線程問題的時(shí)間。
像往常一樣,蘋果的WWDC大會(huì)很豐富,值得一看。sanitizer是Clang編譯器的一部分,在LLVM網(wǎng)站上可以找到更詳細(xì)的信息,在谷歌建立了sanitizer的團(tuán)隊(duì)有許多有趣的wiki頁(yè)面,其中包括了算法用于檢測(cè)線程問題的高層次的演練。
我們使用Swift 3中提供給我們的一個(gè)小的新面貌GCD。蘋果也在“Concurrent Programming With GCD in Swift 3”談話中談到了這個(gè),你可能會(huì)發(fā)現(xiàn)它的用處。此外,Roy Marmelstein寫了一篇很好且簡(jiǎn)潔的帖子闡述這一變化。
本文翻譯自: