iOS开发文集iOS 10 Day By Day: Thread Sanitizer线程检查工具
通常,这些是由于多个线程同时访问相同的一些内存而造成的。我猜想,线程问题是许多开发人员做噩梦的原因。他们是出了名的难以追踪,错误只发生在特定条件下:所以确定问题的根源是非常复杂的。
通常导致线程问题的原因是所谓的“竞争条件”。我们不会去关注太多的细节,像是这意味着什么,而是从谷歌引用ThreadSanitizer手册:
数据竞争发生在当两个线程同时访问同一变量,并且至少有一个访问是编写状态时。
这些用来追踪的是一个绝对的噩梦,但值得庆幸的是Xcode附带一个新的调试工具叫做Thread Sanitizer,甚至可以在你注意到他们之前帮助识别这些问题。
The Project
我们将创建一个简单的应用程序,使我们能够存款和取款100美元面额。像往常一样,项目的完成版本已在GitHub上(为了方便各位读者,小编已经为大家整理了,请点击这里下载)。
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方法已经几乎立即执行,然而,withdraw还需要一段时间才能完成。我们会说这是因为我们需要为取款执行一些欺诈检查,但实际上我们只发送当前线程睡眠2秒。这将给我们后面使用一些多线程提供借口。
唯一需要注意的另一件事是完成模块,这是当存款和取款都成功完成时才执行。
视图控制器
我们的视图控制器由两个按钮——存款和取款,以及一个显示当前余额的标签组成。故事板的布局:
为连接我们的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)" } }
让我们给它一个旋转:
嗯……当我们试着取回钱时有点慢!这是由于我们Account 的withdraw方法及其严格的“欺诈检查”,导致主线程阻塞,直到该方法已经完成。我们希望用户能够以最小的延迟反复点击“Deposit”和“Withdraw”。
救援调度队列
如果我们可以从主线程删除阻塞的withdraw方法,这就太棒了。我们将使用新“Swiftified”中央调度库:
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任务调度到背景队列引发了一个问题。
Thread Sanitizer线程检查工具来拯救我们的理智!
打开检查工具非常简单,只需将你的目标的计划设置和在Diagnostics标签中检查Thread Sanitizer箱。我们可以选择在遇到的问题上暂停,这使得它能够容易地在个案基础上评估每一个问题。我们会这样。
由于线程检查工具只在运行时起作用,我们需要重新编译和重新运行应用程序。让我们开始吧。
在WWDC上,苹果建议在你所有的单元测试开启线程检查工具。检查工具在运行时操作,如果代码执行,只能够确定数据竞争。如果你的代码完全得以单元测试,那么你可能会发现线程检查工具发现了大多数问题,如果不是全部测试,发现的是你的项目的竞态条件 (你会发现我们博客的iOS 9 Day by Day中一个有用的阅读,Xcode 7的代码覆盖工具)。
其他值得注意的是,它只能运行在语言版本3编写的Swift代码上(Objective-C也可兼容),并且只能使用64位模拟器运行。
当我们重复我们之前取款的过程,然后立即存款,线程检查工具会暂停我们的应用程序的执行,因为它发现了竞态条件。这给了我们一个很好的冲突访问发生的地方的堆栈跟踪。
它还将结果输出到控制台,所以你没有必要从Xcode运行检查工具。
通过堆栈跟踪和提供的信息,线程分析仪有助于表明,当访问Account.balance属性时在我们的Account.deposit和Account.withdraw方法中有一个数据竞争。哦,看来我们需要在withdraw和deposit方法中使用相同的串行调度队列:
我们将修改我们的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() } } } }
再次运行应用程序显示了我们仍然有数据竞争,但是它不再是在我们的Account类中,而是由于我们的ViewController从主线程访问balance。
我们可以通过转换到一个只有访问Account的私有变量来保护我们的balance属性,而不是用我们的队列返回balance。
private var _balance: Int = 0 var balance: Int { return queue.sync { return _balance } }
我们需要转换任何书面到平衡变量以使用私有_balance属性。
现在当我们运行我们的应用程序,我们应该能够多次点击“withdraw”和“deposit”而无需令人不安的线程检查工具。太好了,我们刚刚使用这个新工具来修正了我们的错误代码。
进一步的阅读
虽然看起来似乎不像起初那样,线程检查工具可能会成为开发人员工具箱中一个非常重要的iOS工具。它发现数据竞争的能力,即使在程序的运行期间没有发生,也可能会拯救无数小时调试断断续续的线程问题的时间。
像往常一样,苹果的WWDC大会很丰富,值得一看。sanitizer是Clang编译器的一部分,在LLVM网站上可以找到更详细的信息,在谷歌建立了sanitizer的团队有许多有趣的wiki页面,其中包括了算法用于检测线程问题的高层次的演练。
我们使用Swift 3中提供给我们的一个小的新面貌GCD。苹果也在“Concurrent Programming With GCD in Swift 3”谈话中谈到了这个,你可能会发现它的用处。此外,Roy Marmelstein写了一篇很好且简洁的帖子阐述这一变化。
本文翻译自: