你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 如何用Xcode8解決多線程問題

如何用Xcode8解決多線程問題

編輯:IOS開發基礎

77B58PIC8RX_1024.jpg

原文

Xcode 8誕生有段時日了,不知道大家對其中的新Feature是否都學習過一遍了,今天給大家介紹下Xcode 8中一個很實用的特性,Thread Sanitizer,用來解決平時編寫代碼時難以調試的多線程問題,順道梳理下一些常見的容易混淆的多線程概念。

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代碼如下:

01.png

最後計算的結果有很大概率小於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代碼:

02.png

這也屬於data race的場景,一般會出現在對於復雜對象(class或者struct)的多線程寫操作中,原因是因為寫操作本身不是原子的,而且寫操作背後會調用更多的內存操作,多線程同時寫時,會導致這塊內存區間處於中間的不穩定狀態,進而crash,這是真正的惡性的data race。

場景三:亂序

過去幾年Review代碼的經歷中,看到過不少如下使用公共變量來做多線程同步的,比如:

03.png

按理說,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,而是采用如下直白的寫法:

04.png

這種寫法的問題是,多線程環境下,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也是安全的。

05.png

原理我也簡單解釋了一下,但之後有一些朋友私底下和我交流,還是對這一觀點存疑。

實際上,上面的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即可,如下圖:

07.png

這裡要注意的是,Thread Sanitizer現階段只能在模擬器環境下執行,真機還不支持,而且我測試發現,只支持64位系統,也就是說iPhone 5及其更早的模擬器也不支持,iPhone 5s之後才是64位系統。

勾選之後,重新編譯運行代碼即可,我用下面一段代碼做測試:

08.png

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

09.png

很直觀,Xcode直接提示你發生了data race的變量及其代碼位置,同時還清晰的展示了函數當前的各線程調用棧,十分清晰,接下來你要做的就是增加同步操作,比如加鎖,從而消除data race,再運行測試是否生效。

原理

Thread Sanitizer的工作原理在WWDC的視頻中也介紹過了,大家可以仔細看下視頻,大致原理是記錄每個線程訪問變量的信息來做分析,值得一提的是,現階段的Thread Sanitizer最多只同時記錄4個線程的訪問信息,在復雜的場景下,可能出現偶爾檢測不出data race的場景,所以需要長時間經常性的運行來盡可能多的發現data race,這也是為什麼蘋果建議默認開啟Thread Sanitizer,而且Thread Sanitizer造成的額外性能損耗非常之小。

結束語

以上就是Xcode 8新增的多線程問題調試工具Thread Sanitizer,了解背後原理再去使用工具才更得心應手,趕緊拿公司項目跑一跑吧,發現一堆data race可能性一般來說是還是比較高的 :)

QQ截圖20161229225241.png

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved