Xcode Thread Sanitizer 工具使用

想象一个场景,你的 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

更多细节请看原文章

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容