iOS 10 的一个重要更新-线程竞态检测工具 Thread Sanitizer
本文介绍了 Xcode 8 的新出的多线程调试工具 Thread Sanitizer,可以在 app 运行时发现线程竞态。
想想一下,你的 app 已经近乎大功告成:它经过精良的打磨,单元测试全覆盖。只剩下一个问题:有一个很严重的 bug,但是是偶发的,你已经花了好几个小时尝试修复它却一无所获。问题到底出在哪里呀?
这种情况经常是多个线程访问同一块内存造成的。我可以大胆猜测,多线程的 bug 是许多程序员的梦魇。这类 bug 非常难定位,而且只有特定条件下才能重现:所以找出问题的原因确实困难重重。
而问题的原因常常是所谓的『线程竞态』。对这个名词我们不再多费笔墨去解释了,以下摘自 Google 的 ThreadSanitizer 文档:
两个线程同时访问同一个变量,而且其中至少一个线程要做的是写操作,这种情况就叫竞态。
调试竞态问题曾经让程序员们大为头疼;不过值得庆幸的是,Xcode 发布了一个新的线程调试工具叫做 Thread Sanitizer 可以检测出这类问题,甚至比你发现得还早。
建工程
我们做了一个简单的应用,能让用户存钱、取钱,每次 $100。跟之前一样,最终版的工程放在 Github 上了。
银行账户
银行账户的数据模型很简单,名为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
View Controller 里有两个 button ——一个存钱、一个取钱——还有一个 label,显示当前账户余额。Storyboard 中的布局是这样的:

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 主线程。而我们希望的是用户能快速重复存钱、取钱,把延迟降到最低。
Dispatch Queue 登场了
如果要是能把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 来解救我们啦!
开启 Thread Sanitizer 很简单,只需点击 target 的 Edit Scheme…,然后在 Diagnostics tab 下勾选 Thread Sanitizer。可以选择 Pause on issues,这样比较方便一步步调试问题。我们把它勾上。

Edit scheme

勾选 Thread Sanitizer
因为 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 也有一篇短小精悍的博客介绍其中的变化。
iOS 10 的一个重要更新-线程竞态检测工具 Thread Sanitizer的更多相关文章
- iOS 10 的一个重要更新-新的通知推送 API
iOS 10 最重要的变化可能就是通知 API 的重构了.本文用一个简单闹钟的例子介绍了 User Notification 的 API 变化和新功能. 简介 很久以前,开发者就可以在 iOS 里预约 ...
- iOS 10 的一个重要更新-自定义的通知界面
续上篇,在简单闹钟的例子上,在通知界面上显示图片动画,并用通知关联的按钮更新通知界面.介绍 iOS 10 通知 API 的扩展:自定义通知显示界面. 新框架可以统一处理本地通知和远程推送,同时增加了一 ...
- iOS 10 的一个重要更新-开发 iMessage 的第三方插件
苹果官方的 Messages 在 iOS 10 推出了非常重大的更新,可能主要是想从其他 IM 巨头手里抢点市场份额回来,包括 Facebook Messenger, Wechat 和 Snapcha ...
- iOS 10 的一个重要更新-用 UIViewPropertyAnimator 编写动画
曾经的黑暗年代 用基于 block 的 UIView animation 来编写 view 属性(frame, transform 等等)变化的动画非常简单.只需要短短几行代码: view.alpha ...
- 【转】具透 | 你可能不知道,iOS 10 有一个中国「特供」的联网权限功能
9 月底,苹果正式在北京成立了苹果中国研发中心.近几年,我们也在每年更新的 iOS 系统中不断看到,苹果对中国市场的关照.从早前的九宫格输入法,到最近的骚扰电话拦截,都照顾了国内用户的需求. 在 iO ...
- tcping ,一个好用的TCP端口检测工具
1.常用的用法(windows) tcp -w 10 -t -d -i 5 -j --color 81.156.165.66 443 2. http模式 -u,与-h命令连用,每一行输出目标的url ...
- iOS 10 :用 UIViewPropertyAnimator 编写动画
英文:shinobicontrols 译文:戴仓薯 链接:http://www.jianshu.com/p/4244cf130478 [iOS 10 day by day] Day 1:开发 iMes ...
- [iOS 10 day by day] Day 1:开发 iMessage 的第三方插件
本文介绍了 iOS 10 的一个重要更新:Messages 应用支持第三方插件了.作者用一个小游戏作为例子,说明了插件开发从建工程开始,到绘制界面.收发消息的全过程. <iOS 10 day b ...
- iOS 10中如何搭建一个语音转文字框架
在2016WWDC大会上,Apple公司介绍了一个很好的语音识别的API,那就是Speech framework.事实上,这个Speech Kit就是Siri用来做语音识别的框架.如今已经有一些可用的 ...
随机推荐
- 揭秘uc浏览器三
这节我们主要讨论收藏与历史记录页面的边边角角. 首先,看看他的最终的效果图了: 照例了,我们先看看他的布局文件: <!-- tab布局文件 --> <TabHost xmlns:an ...
- Thinkphp5 使用odbc连接到sqlserver
在PHP中访问mssql有两个方式 1.安装相应的驱动,如sqlsrv 2.直接使用PHP自带的ODBC驱动(不需要安装) 本文直接使用自带的ODBC驱动 在使用前请确定PHP开启了php_pdo ...
- 深浅拷贝 python
原文:http://www.jb51.net/article/15714.htm 1. copy.copy 浅拷贝 只拷贝父对象,不会拷贝对象的内部的子对象.2. copy.deepcopy 深拷贝 ...
- Log4net PatternLayout 参数
Log4net PatternLayout 参数 来自: https://logging.apache.org/log4net/log4net-1.2.13/release/sdk/log4net.L ...
- [Functional Programming] Using JS, FP approach with Arrow or State Monad
Using Naive JS: const {modify, get} = require('crocks/State'); const K = require('crocks/combinators ...
- insta经典滤镜下载
好不容易找到的Insta的经典滤镜源码,贴出来帮大家学习. // // IFImageFilter.m // InstaFilters // // Created by Di Wu on 2/28/1 ...
- Skyline开发4-IProject接口
IProject接口可以访问工程设置和打开保存工程的基本方法. 属性 FileVersion:返回 ITEVersionInfo.表示当前运行的TerraExplorer的版本,可通过ITEVersi ...
- iOS界面篇 - bounds和frame的相同和区别
相同点: 他们都是CGRect类型,且拥有属性origin(x, y), size(weight, height) 不同点: bounds是你画的视图的边界,和父视图没有半毛钱关系 frames则一 ...
- WinForm程序开发
WinForm程序开发------------------------------主要页面----------------------------------BaseForm.cs 基类,用于派 ...
- JDK5.0 特性-线程 Condition
来自:http://www.cnblogs.com/taven/archive/2011/12/17/2291471.html import java.util.concurrent.Executor ...