资源竞争与死锁检测

多线程编程一直是一个非常难的话题,而资源竞争和死锁问题则是比较常见的多线程问题,这里我们来看看如何检测这些问题。

LLVM

其实llvm项目自身就有这两者的检测方法。而在xcode中也集成了该功能,要使用也非常简单,选中Thread Sanitizer,并且重新编译运行即可。

image

那么接下来我们来看看使用情况以及他们是如何实现的。

Data Race

数据竞争是我们非常容易犯的一个错误,而且出现问题了也非常难解决。因为出现的概率并不高,而且出现了问题也不会直接表现出来,而可能是通过其他方式表现出来。

首先我们来看一个非常简单的数据竞争问题:

char g_char;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self setCharB];
    });
    [self setCharA];
}

- (void)setCharA {
    g_char = 'a';
}

- (void)setCharB {
    g_char = 'b';
}

虽然更新一个字节这种操作非常简单,但依然需要在这里加上锁,如果没有加上则会报告如下错误:

==================
WARNING: ThreadSanitizer: data race (pid=62345)
  Write of size 1 at 0x0001032e0f10 by thread T2:
    #0 -[ViewController setCharB] ViewController.m:35 (MallocTest:x86_64+0x100001499)
    #1 __29-[ViewController viewDidLoad]_block_invoke ViewController.m:26 (MallocTest:x86_64+0x1000013da)
    #2 __tsan::invoke_and_release_block(void*) <null>:2136816 (libclang_rt.tsan_iossim_dynamic.dylib:x86_64+0x622bb)
    #3 _dispatch_client_callout <null>:2136816 (libdispatch.dylib:x86_64+0x3847)

  Previous write of size 1 at 0x0001032e0f10 by main thread:
    #0 -[ViewController setCharA] ViewController.m:32 (MallocTest:x86_64+0x100001472)
    #1 -[ViewController viewDidLoad] ViewController.m:28 (MallocTest:x86_64+0x100001381)
    #2 -[UIViewController loadViewIfRequired] <null>:2136816 (UIKit:x86_64+0x1ce190)
    #3 start <null>:2136816 (libdyld.dylib:x86_64+0x1954)

  Location is global 'g_char' at 0x0001032e0f10 (MallocTest+0x000100003f10)

  Thread T2 (tid=1422519, running) is a GCD worker thread

SUMMARY: ThreadSanitizer: data race ViewController.m:35 in -[ViewController setCharB]
==================

同时在左边的导航栏里会显示如下结果:

image

那么LLVM是怎么实现的呢?

资源竞争的检测其实分为两部分,一部分是编译期的处理,另一部分是运行期的监控。

编译期,编译器会在数据访问的时候插入一段代码,来告诉检测器具体的数据访问情况。这个效果可以看具体的汇编:

-[ViewController setCharA]:
    0x106bd4448 <+0>:  pushq  %rbp
    0x106bd4449 <+1>:  movq   %rsp, %rbp
    0x106bd444c <+4>:  movq   0x8(%rbp), %rdi
    0x106bd4450 <+8>:  callq  0x106bd4798               ; symbol stub for: __tsan_func_entry
    0x106bd4455 <+13>: leaq   0x2afc(%rip), %rdi        ; g_char
    0x106bd445c <+20>: callq  0x106bd47bc               ; symbol stub for: __tsan_write1
    0x106bd4461 <+25>: movb   $0x61, 0x2af0(%rip)       ; lock + 63
    0x106bd4468 <+32>: callq  0x106bd479e               ; symbol stub for: __tsan_func_exit
    0x106bd446d <+37>: popq   %rbp
    0x106bd446e <+38>: retq   

运行期的监控则是靠动态库来导入的(在早期是依赖于静态库)。

可以看到,需要做到在编译期插入代码,不禁会想已经编译好的二进制该怎么办?这里我们来看两个例子:

CoreFoundation`-[__NSArrayM addObject:]:
    ...
    0x10e5b0d82 <+18>: leaq   0x3a3fa7(%rip), %rax      ; __cf_tsanWriteFunction
    ...

在NSMutableArray的代码中,我们发现有一个方法很可疑__cf_tsanWriteFunction,这个方法似乎就是上面的__tsan_write1方法的objc版。同时这个方法在真机上是没有的。

pthread_mutex_lock(&lock)在该模式下实际对应的方法是libclang_rt.tsan_iossim_dynamic.dylib wrap_pthread_mutex_lock,同时dispatch_sync对应的方法是libclang_rt.tsan_iossim_dynamic.dylib wrap_dispatch_sync,可以知道他们都来源于一个非标准的动态库,这也就是说明在该模式下,系统会给我们链接一个已经编译好的,插入相应代码的动态库。这也代表着如果你引用了第三方二进制库,不一定能够检测出其中的竞争问题。

这里还需要检测到线程的状态,则是使用了pthread的一个公开接口:

typedef void (*pthread_introspection_hook_t)(unsigned int event, pthread_t thread, void *addr, size_t size);

enum {
  PTHREAD_INTROSPECTION_THREAD_CREATE = 1,
  PTHREAD_INTROSPECTION_THREAD_START,
  PTHREAD_INTROSPECTION_THREAD_TERMINATE,
  PTHREAD_INTROSPECTION_THREAD_DESTROY,
};

pthread_introspection_hook_install(pthread_introspection_hook);
算法

这个的检测算法较为复杂,这里简单的来描述一下。

  • 首先每一个数据根据其内存地址与访问线程id都会有一个对应的内存区块来保存其访问数据,一般是8 bytes映射为1 bytes,所以这里的内存分配器也是需要进行相应的修改。
  • 将当前状态和已保存的数据进行比较。
  • 如果是非同一个线程,并且已保存的数据访问时间是在当前访问时间之后。
  • 那么认为这是一次资源竞争。

Dead lock

死锁的检测相对比较简单了,他并不需要编译期的介入,而是纯运行时的检测。不过遗憾的是xcode上并没有集成,可能是觉得死锁本身就会严重阻碍程序运行,容易被察觉吧。

主要需要做的是hook掉所有锁相关的api,掌管willLockdidLock的消息,LLVM提供默认hook了pthread的相关接口。

每次加锁之前都会产生一个锁-线程的匹配,加锁之后释放该锁-线程的匹配。

如果A锁被某线程持有,同时B锁也被该线程持有,那么就形成了A=>B的一个关联,如果这样的关联形成了一个环,那么就说明产生了死锁。该方法可以利用邻接二维矩阵来实现高效的查找。

如果恢复产生的死锁问题呢?这里我没有找到更好的办法,只能做以下两种处理:

  • 杀死某个非主线程的线程,这样能够解除死锁,但会引起资源泄露和逻辑缺失的问题。
  • 直接返回,可能会引起资源竞争的问题。

参考

The "Double-Checked Locking is Broken" Declaration
Finding races and memory errors with compiler instrumentation.
ThreadSanitizerAlgorithm
llvm-compiler-rt
valgrind

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

推荐阅读更多精彩内容

  • 引用自多线程编程指南应用程序里面多个线程的存在引发了多个执行线程安全访问资源的潜在问题。两个线程同时修改同一资源有...
    Mitchell阅读 1,929评论 1 7
  • 2016年国庆假期终于把此书过完,整理笔记和体会于此。 关于书名 书名源于俄罗斯的演员斯坦尼斯拉夫斯基创作的《演员...
    李剑飞的简书阅读 7,124评论 2 65
  • 翻译:Synchronization 同步 应用程序中存在多个线程会导致潜在的问题,这些问题可能会导致从多个执行线...
    AlexCorleone阅读 2,374评论 0 4
  • 突如其来的暴雪,让格拉斯哥这座已经快二十年没有下过大雪的城市措手不及。 在前一天的夜晚就已经发现了端倪,明明应该已...
    无与伦比的机智阅读 260评论 0 2
  • 曾经以为我长大了,懂事了,可以做好很多事情,也可以承担起很多责任。但,我还是太幼稚了。其实,我一直都还是那个懵懂无...
    雪雪Y阅读 364评论 0 1