如何用Xcode8解決多線程問題

Thread Sanitizer
這款工具集成在Xcode 8中,主要幫助定位多線程相關的問題,還沒有了解過的同學可以先查看 WWDC 2016 Session 412。官方的介紹當中它可以查出以下多線程相關的問題:
Use of uninitialized mutexes
Thread leaks (missing pthread_join)
Unsafe calls in signal handlers (ex:malloc)
Unlock from wrong thread
Data races

前面四項出現的場景較少,真正體現這款工具強大之處的是最后一項,檢查data races,也是我們平時寫多線程代碼時最容易遇到的問題,一旦踩坑,現象往往是偶現的,難以調試。
在開始介紹Thread Sanitizer如何使用之前,我們應該先花點時間了解下什么是data race,以及它到底有什么危害,建議先看下我之前寫過的一篇關于iOS多線程安全的文章
data race的定義很簡單:當至少有兩個線程同時訪問同一個變量,而且至少其中有一個是寫操作時,就發生了data race。這段定義只是描述了什么是data race,卻沒有說明data race會帶來什么嚴重后果,這是因為data race可能會造成多種影響,而且有些影響不一定是致命的(比如crash)。data race也不是什么罕見的場景,只要涉及到多線程編程,遇到的概率非常之高,下面我們看一些data race具體的例子及其危害。
場景一:計算出錯
這也是大學課程里經常舉例的一個場景,Objective C代碼如下:

__block int count =0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{ 
for (int i = 0; i < 10000; i ++) { count ++; }
});
for (int i = 0; i < 10000; i ++) { count ++;}

最后計算的結果有很大概率小于20000,原因是count ++
為非原子操作。這也是data race的場景,這種race沒有crash也沒有memory corruption,因此有些人把這種race稱作benign race(良性的race)。不過上面提到的WWDC視頻中,蘋果的工程師說到:
There is No Such Thing as a “Benign” Race

意思是,只要發生data race,就沒有良性一說了,因為雖然程序沒有crash,但count最后的值還是出錯了,這種 錯誤必然會導致邏輯上的錯誤,如果這個count值代表的是你銀行卡余額,你應該會更加同意蘋果工程師的觀點。
場景二:Crash!
這種場景是真正會導致crash和memory corruption的,發生在兩個線程同時對同一個變量執行寫操作時,比如如下Objective C代碼:

NSMutableString* str = [@"" mutableCopy];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ for (int i = 0; i < 10000; i ++) { [str setString:@"1234567890"]; }});
for (int i = 0; i < 10000; i ++) { [str setString:@"abcdefghigk"];}

這也屬于data race的場景,一般會出現在對于復雜對象(class或者struct)的多線程寫操作中,原因是因為寫操作本身不是原子的,而且寫操作背后會調用更多的內存操作,多線程同時寫時,會導致這塊內存區間處于中間的不穩定狀態,進而crash,這是真正的惡性的data race。
場景三:亂序
過去幾年Review代碼的經歷中,看到過不少如下使用公共變量來做多線程同步的,比如:

//thread 1count = 10;countFinished = true;//thread 2while (countFinished == false) { usleep(1000);}NSLog(@"count: %d", count);

按理說,count最后會輸出值10。可實際上,編譯器并不知道thread 2對count
和countFinished
這兩個變量的賦值順序有依賴,所以基于優化的目的,有可能會調整thread 1中count = 10;
和countFinished = true;
生成的最后指令的執行順序,最后也就導致count值輸出的時機不對,雖然最后count的值還是10。這也可以看做是一種benign race,因為也不會crash,而是程序的流程出錯。而且這種錯誤的調試及其困難,因為邏輯上是完全正確的,不明白其中緣由的同學甚至會懷疑是系統bug。
遇到這種多線程讀寫狀態,而且存在順序依賴的場景,不能簡單依賴代碼邏輯。解決這種data race場景有一個簡單辦法:加鎖,比如使用NSLock,將對順序有依賴的代碼塊整個原子化,加鎖之所以有用是因為會生成memory barrier,從而避免了編譯器優化。
場景四:內存泄漏
iOS剛誕生不久時,還沒有多少Best Practise,不少人寫單例的時候還不會用到dispatch_once_t,而是采用如下直白的寫法:

Singleton *getSingleton() { static Singleton *sharedInstance = nil; if (sharedInstance == nil) { sharedInstance = [[Singleton alloc] init]; } return sharedInstance;}

這種寫法的問題是,多線程環境下,thread A和thread B會同時進入sharedInstance = [[Singleton alloc] init];
,Singleton被多創建了一次,MRC環境就產生了內存泄漏。
這是個經典的例子,也是data race的場景之一,其結果是造成額外的內存泄漏,這種race也可以算作是benign的,但也是我們平時編寫代碼應該避免的。
上面幾個是我們寫iOS代碼比較容易遇到的,還有其他一些就不一一舉例了,只要理解了data race的含義都不難分析這些race導致的具體問題。
BOOL是否多線程安全?
在之前那篇iOS多線程安全的文章中,我提到對于BOOL類型的property來說,聲明為atomic并沒有意義,nonatmoic對于BOOL的get,set也是安全的。

@property (nonatomic, assign) BOOL isValid;

原理我也簡單解釋了一下,但之后有一些朋友私底下和我交流,還是對這一觀點存疑。
實際上,上面的WWDC視頻中,蘋果的工程師也提到了這一點:有些人認為pointer sized的變量操作時是天然多線程安全的。所謂的pointer size也就是我們指針變量的大小,64位系統為8字節。這位工程師提到,這種看法是問題的,理由如下:

On some architectures (ex., x86) reads and writes are atomic
But even a “benign” race is undefined behavior in C
May cause issues with new compilers or architectures

C標準對于這種行為定義是undefined behavior,意思是最后的結果是不確定的,不同的編譯器針對不同的CPU架構所產生的最后執行文件,其執行結果是沒有規定的,如果有哪個硬件平臺上出現了crash,那么也沒有違背C的標準,因為C沒有規定其一定是原子操作。
同時,據我所知(扒過一些資料),以及我這么些年寫iOS代碼的經歷,nonatomic修飾的BOOL確實是原子操作且多線程安全的,我也沒找到什么樣的CPU架構下,pointer sized的變量是非原子操作的。
所以,更準確更嚴格的說法應該是:現階段的iOS設備軟硬件環境下,BOOL的讀寫是原子的,不過將來不一定,蘋果官方和C標準都沒有做任何保證
如何使用Thread Sanitizer
啟用Thread Sanitizer的方式很簡單,只需要在Xcode的scheme中勾選Thread Sanitizer即可,如下圖:


這里要注意的是,Thread Sanitizer現階段只能在模擬器環境下執行,真機還不支持,而且我測試發現,只支持64位系統,也就是說iPhone 5及其更早的模擬器也不支持,iPhone 5s之后才是64位系統。
勾選之后,重新編譯運行代碼即可,我用下面一段代碼做測試:

__block int count = 0;dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ for (int i = 0; i < 10000; i ++) { count ++; }});for (int i = 0; i < 10000; i ++) { count ++;}

運行之后會在Xcode中出現如下提示:

![](http://upload-images.jianshu.io/upload_images/1288444-cbb4265b40f2fe19.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240

很直觀,Xcode直接提示你發生了data race的變量及其代碼位置,同時還清晰的展示了函數當前的各線程調用棧,十分清晰,接下來你要做的就是增加同步操作,比如加鎖,從而消除data race,再運行測試是否生效。
原理
Thread Sanitizer的工作原理在WWDC的視頻中也介紹過了,大家可以仔細看下視頻,大致原理是記錄每個線程訪問變量的信息來做分析,值得一提的是,現階段的Thread Sanitizer最多只同時記錄4個線程的訪問信息,在復雜的場景下,可能出現偶爾檢測不出data race的場景,所以需要長時間經常性的運行來盡可能多的發現data race,這也是為什么蘋果建議默認開啟Thread Sanitizer,而且Thread Sanitizer造成的額外性能損耗非常之小。
轉自http://mrpeak.cn/blog/thread-sanitizer/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容