多線程編程一直是一個非常難的話題寝姿,而資源競爭和死鎖問題則是比較常見的多線程問題嗡综,這里我們來看看如何檢測這些問題。
LLVM
其實llvm項目自身就有這兩者的檢測方法只壳。而在xcode中也集成了該功能俏拱,要使用也非常簡單,選中Thread Sanitizer
吼句,并且重新編譯運行即可锅必。
那么接下來我們來看看使用情況以及他們是如何實現(xiàn)的。
Data Race
數(shù)據(jù)競爭是我們非常容易犯的一個錯誤惕艳,而且出現(xiàn)問題了也非常難解決搞隐。因為出現(xiàn)的概率并不高,而且出現(xiàn)了問題也不會直接表現(xiàn)出來远搪,而可能是通過其他方式表現(xiàn)出來劣纲。
首先我們來看一個非常簡單的數(shù)據(jù)競爭問題:
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';
}
雖然更新一個字節(jié)這種操作非常簡單,但依然需要在這里加上鎖谁鳍,如果沒有加上則會報告如下錯誤:
==================
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]
==================
同時在左邊的導航欄里會顯示如下結果:
那么LLVM是怎么實現(xiàn)的呢癞季?
資源競爭的檢測其實分為兩部分,一部分是編譯期的處理倘潜,另一部分是運行期的監(jiān)控绷柒。
編譯期,編譯器會在數(shù)據(jù)訪問的時候插入一段代碼窍荧,來告訴檢測器具體的數(shù)據(jù)訪問情況辉巡。這個效果可以看具體的匯編:
-[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
運行期的監(jiān)控則是靠動態(tài)庫來導入的(在早期是依賴于靜態(tài)庫)。
可以看到蕊退,需要做到在編譯期插入代碼郊楣,不禁會想已經(jīng)編譯好的二進制該怎么辦憔恳?這里我們來看兩個例子:
CoreFoundation`-[__NSArrayM addObject:]:
...
0x10e5b0d82 <+18>: leaq 0x3a3fa7(%rip), %rax ; __cf_tsanWriteFunction
...
在NSMutableArray的代碼中,我們發(fā)現(xiàn)有一個方法很可疑__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
程梦,可以知道他們都來源于一個非標準的動態(tài)庫,這也就是說明在該模式下橘荠,系統(tǒng)會給我們鏈接一個已經(jīng)編譯好的屿附,插入相應代碼的動態(tài)庫。這也代表著如果你引用了第三方二進制庫哥童,不一定能夠檢測出其中的競爭問題挺份。
這里還需要檢測到線程的狀態(tài),則是使用了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);
算法
這個的檢測算法較為復雜贮懈,這里簡單的來描述一下匀泊。
- 首先每一個數(shù)據(jù)根據(jù)其內(nèi)存地址與訪問線程id都會有一個對應的內(nèi)存區(qū)塊來保存其訪問數(shù)據(jù),一般是8 bytes映射為1 bytes朵你,所以這里的內(nèi)存分配器也是需要進行相應的修改各聘。
- 將當前狀態(tài)和已保存的數(shù)據(jù)進行比較。
- 如果是非同一個線程抡医,并且已保存的數(shù)據(jù)訪問時間是在當前訪問時間之后躲因。
- 那么認為這是一次資源競爭。
Dead lock
死鎖的檢測相對比較簡單了魂拦,他并不需要編譯期的介入毛仪,而是純運行時的檢測。不過遺憾的是xcode上并沒有集成芯勘,可能是覺得死鎖本身就會嚴重阻礙程序運行箱靴,容易被察覺吧。
主要需要做的是hook掉所有鎖相關的api荷愕,掌管willLock
和didLock
的消息衡怀,LLVM提供默認hook了pthread的相關接口。
每次加鎖之前都會產(chǎn)生一個鎖-線程
的匹配安疗,加鎖之后釋放該鎖-線程
的匹配抛杨。
如果A鎖被某線程持有,同時B鎖也被該線程持有荐类,那么就形成了A=>B
的一個關聯(lián)怖现,如果這樣的關聯(lián)形成了一個環(huán),那么就說明產(chǎn)生了死鎖。該方法可以利用鄰接二維矩陣
來實現(xiàn)高效的查找屈嗤。
如果恢復產(chǎn)生的死鎖問題呢潘拨?這里我沒有找到更好的辦法,只能做以下兩種處理:
- 殺死某個非主線程的線程饶号,這樣能夠解除死鎖铁追,但會引起資源泄露和邏輯缺失的問題。
- 直接返回茫船,可能會引起資源競爭的問題琅束。
參考
The "Double-Checked Locking is Broken" Declaration
Finding races and memory errors with compiler instrumentation.
ThreadSanitizerAlgorithm
llvm-compiler-rt
valgrind