想象一个场景,你的 App 已经准备发布, 就是有一个问题,有个 严重的 bug 断断续续的出现,而你已经花了几个小时去修复它,它到底是什么?
通常, 这些都是多个线程同时访问内存中的同一段地址造成的。相信线程问题是许多开发人员的噩梦。它们难以跟踪,因为错误只发生在某些条件下,所以确定问题的根本原因可能是非常棘手的。通常的原因是所谓的“race condition”。
当两个线程并发访问同一个变量,并且至少有一个访问是写时,会发生数据竞争。
Thread Sanitizer (TSan)
跟踪数据竞争在过去是一个绝对的噩梦,但幸运的是Xcode已经发布了一个新的调试工具,称为Thread Sanitizer,可以帮助识别这些问题,你甚至注意到之前。
TSan作为一个能够检查线程错误的工具, 它现在能检查哪些类型的错误呢
- Use of uninitialized mutexes
- Thread leaks (missing phread_johin)
- Unsafe calls in signal handlers (ex:malloc)
- Unlock from wrong thread
- Data race
使用Thread Sanitizer(TSan)线程检查工具
打开检查工具非常简单,只需将你的目标的计划设置和在Diagnostics标签中检查Thread Sanitizer箱。
我们可以选择在遇到的问题上暂停,这使得它能够容易地在个案基础上评估每一个问题。
或者可以不暂停,稍后通过点击状态栏上的 Runtime Issue 处理
检查 uninitialized mutexes
这段代码的意思是,我们在 viewDidLoad 方法里面重新 reset 自己的状态, 为了防止多个线程去访问同一个 dataArray 属性, 造成 data race 的状态, 我们在 resetStatus 的时候需要加锁
- (void)viewDidLoad {
[super viewDidLoad];
[self resetStatue];
pthread_mutex_init(&(_mutex), NULL);
}
- (void)resetStatue{
[self acquireLock];
self.dataArray = nil;
[self releaseLock];
}
- (void)acquireLock{
pthread_mutex_lock(&_mutex);
}
- (void)releaseLock{
pthread_mutex_unlock(&_mutex);
}
但当前代码中,我们实际上调用的是一个没有初始化的锁( init 方法在 resetStatus 方法下面哦), 但这段代码在实际运行的过程中,百分之九十九也不会出现crash, 但有了TSan后, 我们来看看发生了什么变化
在 Issue Navigator 中, TSan 明确的告诉了我们错误的类型, 而且把线程中的历史信息都记录了下来以便我们分析并解决这个问题
检查 Data Race
我们将创建一个简单的应用程序,使我们能够存款和取款100美元面额。
// 存款, deposit方法已经几乎立即执行
- (void)depositAmount:(NSInteger)amount onSuccess:(void (^)())onSuccess
{
NSInteger newBalance = self.balance + amount;
self.balance = newBalance;
onSuccess ? onSuccess () : nil;
}
// 取款,然而,withdraw 需要一段时间才能完成, 我们会说这是因为我们需要为取款执行一些欺诈检查
- (void)withdrawAmount:(NSInteger)amount onSuccess:(void (^)())onSuccess
{
//我们希望用户能够以最小的延迟反复点击“Deposit”和“Withdraw”,所以使用了调度队列。
dispatch_async(dispatch_queue_create("com.qingyv.balance-moderator", nil), ^{
NSInteger newBalance = self.balance - amount;
if (newBalance < 0) {
NSLog(@"You don't have enough money to withdraw %@", @(amount));
return ;
}
sleep(2); //实际上我们只是让当前线程 sleep 2秒。
self.balance = newBalance;
dispatch_async(dispatch_get_main_queue(), ^{
onSuccess ? onSuccess() : nil;
});
});
}
操作 :
快速点击,存100 -> 取100 -> 存100 就会引起数据竞争,并触发Thread Sanitizer。
原理
总结
- TSan 是一个检查 Runtime Issues 的工具 (不支持静态检查)
- 只能运行在语言版本3编写的Swift代码上 (Objective-C也可兼容),
- 只能在64位macOS 或 64位模拟器上运行 (所有真机设备都不可以用来调试)。
源码
VC
// QYThreadSanitizerViewController.h
@interface QYThreadSanitizerViewController : UIViewController
@end
// QYThreadSanitizerViewController.m
#import "QYThreadSanitizerViewController.h"
#import "QYAccount.h"
@interface QYThreadSanitizerViewController ()
@property (nonatomic, strong) UILabel *balanceLabel;
@property (nonatomic, strong) UIButton *withdrawButton;
@property (nonatomic, strong) UIButton *depositButton;
@property (nonatomic, strong) QYAccount *account;
@end
@implementation QYThreadSanitizerViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setupUI];
[self updateBalanceLabel];
}
- (void)setupUI
{
[self.view addSubview:self.balanceLabel];
[self.view addSubview:self.withdrawButton];
[self.view addSubview:self.depositButton];
self.view.backgroundColor = [UIColor grayColor];
}
- (void)updateBalanceLabel
{
self.balanceLabel.text = [NSString stringWithFormat:@"Balance: $%@", @(self.account.balance)];
}
- (void)withdrawClicked:(id)sender
{
[self.account withdrawAmount:100 onSuccess:^{
[self updateBalanceLabel];
}];
}
- (void)despositClicked:(id)sender
{
[self.account depositAmount:100 onSuccess:^{
[self updateBalanceLabel];
}];
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
[self.balanceLabel sizeToFit];
self.balanceLabel.width = 200;
self.balanceLabel.top = 100;
self.balanceLabel.centerX = self.view.centerX;
self.depositButton.width = 200;
self.depositButton.height = 30;
self.depositButton.top = self.balanceLabel.bottom + 50;
self.depositButton.centerX = self.balanceLabel.centerX;
self.withdrawButton.width = 200;
self.withdrawButton.height = 30;
self.withdrawButton.top = self.depositButton.bottom + 30;
self.withdrawButton.centerX = self.balanceLabel.centerX;
}
#pragma mark - Lazy Init
- (QYAccount *)account
{
if (!_account) {
_account = [QYAccount new];
}
return _account;
}
- (UILabel *)balanceLabel
{
if (!_balanceLabel) {
_balanceLabel = [UILabel new];
_balanceLabel.font = [UIFont systemFontOfSize:30];
_balanceLabel.textAlignment = NSTextAlignmentCenter;
_balanceLabel.backgroundColor = [UIColor whiteColor];
}
return _balanceLabel;
}
- (UIButton *)withdrawButton
{
if (!_withdrawButton) {
_withdrawButton = [UIButton new];
[_withdrawButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[_withdrawButton setTitle:@"取$100" forState:UIControlStateNormal];
[_withdrawButton addTarget:self action:@selector(withdrawClicked:) forControlEvents:UIControlEventTouchUpInside];
_withdrawButton.backgroundColor = [UIColor whiteColor];
}
return _withdrawButton;
}
- (UIButton *)depositButton
{
if (!_depositButton) {
_depositButton = [UIButton new];
[_depositButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[_depositButton setTitle:@"存$100" forState:UIControlStateNormal];
[_depositButton addTarget:self action:@selector(despositClicked:) forControlEvents:UIControlEventTouchUpInside];
_depositButton.backgroundColor = [UIColor whiteColor];
}
return _depositButton;
}
@end
Account
// QYAccount.h
@interface QYAccount : NSObject
@property (nonatomic, assign) NSInteger balance;
// 存款
- (void)depositAmount:(NSInteger)amount onSuccess:(void (^)())onSuccess;
// 取款
- (void)withdrawAmount:(NSInteger)amount onSuccess:(void (^)())onSuccess;
@end
//QYAccount.m
#import "QYAccount.h"
@implementation QYAccount
// 存款
- (void)depositAmount:(NSInteger)amount onSuccess:(void (^)())onSuccess
{
NSInteger newBalance = self.balance + amount;
self.balance = newBalance;
onSuccess ? onSuccess () : nil;
}
// 取款
- (void)withdrawAmount:(NSInteger)amount onSuccess:(void (^)())onSuccess
{
dispatch_async(dispatch_queue_create("com.qingyv.balance-moderator", nil), ^{
NSInteger newBalance = self.balance - amount;
if (newBalance < 0) {
NSLog(@"You don't have enough money to withdraw %@", @(amount));
return ;
}
sleep(2);
self.balance = newBalance;
dispatch_async(dispatch_get_main_queue(), ^{
onSuccess ? onSuccess() : nil;
});
});
}
@end
更多细节请看原文章
参考
- iOS 10 Day by Day: Thread Sanitizer
- WWDC2016