本文介绍了 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
译者:戴仓薯