作者 | 張璇、張凱
審校 | 重樓
一、背景與意義
在系統應用開發中,隨著應用程序復雜度的增加,多線程編程成為了提升用戶體驗和應用性能的關鍵技術之一,線程同步占據著舉足輕重的地位。鑒于iOS應用程序普遍涉及多任務處理和并發操作,確保線程安全成為一項至關重要的任務。Objective-C語言為此提供了一種高效且可靠的同步機制,即@synchronized關鍵字。這一機制通過標記特定代碼段為臨界區,確保了同一時刻僅有一個線程能夠執行該代碼塊,從而有效保護了數據的一致性和完整性。
此外,@synchronized關鍵字在多線程環境中還具備遞歸鎖定的能力。這意味著同一線程可以多次獲取同一把鎖,而不會導致死鎖的產生,極大地提升了遞歸調用的安全性和可行性。
二、關鍵技術
在iOS多線程環境中,@synchronized關鍵字是實現線程同步和遞歸鎖定的關鍵技術。其內部實現原理基于Objective-C的運行時(Runtime)和底層的鎖機制。
首先,@synchronized關鍵字在編譯時會被轉換成Objective-C運行時庫中的一個函數調用,即objc_sync_enter和objc_sync_exit。這兩個函數分別用于獲取和釋放鎖。當線程嘗試進入@synchronized代碼塊時,objc_sync_enter函數會被調用,并嘗試獲取與給定對象相關聯的鎖。如果鎖已經被其他線程持有,則當前線程將被阻塞,直到鎖被釋放。如果鎖成功獲取,則線程可以安全地執行@synchronized代碼塊中的代碼。
其次,@synchronized關鍵字的遞歸鎖定能力是通過在運行時維護一個線程到鎖計數器的映射來實現的。當同一線程多次進入同一個@synchronized代碼塊時,鎖計數器會遞增,而不是重新獲取鎖。這樣,即使線程多次進入臨界區,也不會導致死鎖。當線程離開@synchronized代碼塊時,鎖計數器會遞減,直到計數器歸零,此時鎖才會被釋放,允許其他線程進入臨界區。
2.1 @synchronized的源碼入口
首先,通過一個demo來了解下@synchronized的具體結構:
圖1 demo Objective-C語言版本示例
在xcode中,可以通過clang -rewrit-objc命令,將上述的demo代碼重寫為C++代碼,這里將關鍵代碼提取如下:
圖2 demo C++語言版本示例
顯然,從給定信息中可以明確,_sync_exit()函數觸發了_SYNC_EXIT的構造過程,而 ~_SYNC_EXIT是_SYNC_EXIT的析構函數。從根本上講,@synchronized指令的底層實現依賴于對objc_sync_enter和objc_sync_exit這兩個函數的調用。接下來,我們將深入解析 objc_sync_enter 和 objc_sync_exit的具體實現方式。
2.2、objc_sync_enter 和 objc_sync_exit函數實現
objc_sync_enter和objc_sync_exit是 Objective-C 運行時庫中用于處理同步鎖的關鍵函數。這兩個函數通過底層的鎖機制(如互斥鎖或自旋鎖)來確保在同一時刻,只有一個線程能夠執行特定的代碼塊。下面,我們將詳細解析這兩個函數的實現方式及其背后的原理。
2.2.1 objc_sync_enter 函數的實現
objc_sync_enter函數的主要作用是嘗試獲取與給定對象相關聯的鎖。如果鎖已被其他線程持有,則當前線程將被阻塞,直到鎖被釋放。這個函數的實現通常包括以下幾個步驟:
1. 計算鎖對象的哈希值:首先,根據傳入的對象(通常是作為@synchronized語句中鎖的唯一標識符),計算出一個哈希值。這個哈希值用于在內部的數據結構中快速定位到對應的鎖對象。
2.查找或創建鎖對象:在內部的數據結構中(如哈希表或鏈表),根據計算出的哈希值查找是否存在對應的鎖對象。如果不存在,則創建一個新的鎖對象并插入到數據結構中。這個鎖對象可能是一個封裝了互斥鎖或自旋鎖等底層同步機制的結構體。
3. 嘗試獲取鎖:使用找到的鎖對象,嘗試獲取鎖。這通常涉及到底層同步機制的調用,如調用互斥鎖的lock方法或自旋鎖的相關操作。如果鎖已被其他線程持有,則當前線程將被阻塞。
4.記錄線程與鎖的關系:為了支持遞歸鎖定,Objective-C 運行時還需要記錄哪些線程已經持有了哪些鎖,以及持有鎖的次數。這通常是通過一個線程到鎖計數器的映射來實現的。
圖3 objc_sync_enter函數源碼
2.2.2 objc_sync_exit 函數的實現
objc_sync_exit函數的主要作用是釋放之前通過objc_sync_enter獲取的鎖。這個函數的實現通常包括以下幾個步驟:
1. 計算鎖對象的哈希值:與objc_sync_enter相同,首先根據傳入的對象計算哈希值,以找到對應的鎖對象。
2. 查找鎖對象:在內部的數據結構中查找對應的鎖對象。
3. 釋放鎖:使用找到的鎖對象,調用其釋放鎖的方法(如互斥鎖的unlock方法)。這將允許其他被阻塞的線程進入臨界區。
4. 更新線程與鎖的關系:如果當前線程是最后一次釋放該鎖(即鎖計數器減至零),則從線程到鎖計數器的映射中移除該線程的記錄。這確保了遞歸鎖定的正確性。
圖4 objc_sync_exit函數源碼
2.3 總結
從深入探索objc_sync_enter和objc_sync_exit兩個函數的源代碼中,我們可以清晰地看到這兩個函數在同步機制中的核心作用。它們通過巧妙地利用互斥鎖data->mutex,確保了線程安全地進入和退出同步塊。這種機制在并發編程中至關重要,能夠防止多個線程同時訪問共享資源,從而避免了數據競爭和不一致性的風險。
進一步地,我們發現data這個變量實際上是指向一個SyncData類型實例對象的指針。在這里,SyncData扮演了至關重要的角色,它封裝了與同步操作相關的所有必要信息,包括互斥鎖、同步狀態等。通過精心設計的SyncData結構體,我們能夠更加靈活地管理和控制同步資源的訪問,確保程序在不同線程之間的協調運行。
與此同時,id2data這一組件也引起了我們的關注。從名字上推測,它似乎是一個用于將某種標識符(如對象ID)映射到對應SyncData實例的函數或方法。在并發編程中,這樣的映射關系對于快速定位和管理同步資源至關重要。通過id2data,我們可以根據傳入的obj(可能是某個對象的唯一標識符)快速地查找到對應的data(即該對象的同步數據)。這種映射機制大大提高了同步操作的效率和準確性,為程序的并發執行提供了強有力的支持。
為了更深入地理解SyncData結構體和id2data的具體實現,我們將在接下來的第三小節中詳細探討SyncData的結構和成員變量,以及它在同步機制中的具體應用。同時,在第四小節中,我們將解析id2data的實現細節,包括它如何接收輸入參數、進行映射查找以及返回對應的SyncData實例。通過對這些核心組件的深入理解,我們將能夠更好地掌握Objective-C的同步機制,并在實際開發中靈活運用。
三、SyncData結構體介紹與解析
首先,讓我們對SyncData結構體的實現進行了解:
圖5 SyncData基本結構體
從這段源碼中可以看到SyncData的基本結構,本質上是存放了一個傳入對象obj的單向鏈表、一把遞歸鎖、以及使用的線程數量??梢詮南旅娴倪@段源碼中看一下這把遞歸鎖。這把遞歸鎖本質上是基于os_unfair_lock的封裝。這里補充下:os_unfair_lock是iOS中的一把互斥鎖。在之前的版本中recursive_mutex_t是由pthread_mutex_t來進行封裝的,因此這里只需要將其理解成一把互斥鎖即可。
圖6 recursive_mutex_t基本結構
四、id2data的底層實現的分析與研究
在正式深入分析id2data的詳盡細節之際,為了構建一個更為明晰的認知框架,本文將首先對所涉及的數據結構進行一次宏觀層面的梳理與整體性闡述。此舉旨在為后續針對id2data的深入探討奠定堅實的背景知識基礎,確保各位讀者能夠依托這一穩固的基石,更加精準地把握相關概念與邏輯脈絡。
4.1 SyncCache的底層實現邏輯
圖7 SyncCache基本結構
可以明顯地觀察到,SyncCache容器中存儲的是SyncData類型的數據。SyncData這個結構體在文章的第三部分已經進行了詳細的說明,因此在這里就不再重復解釋了。實際上,從這個細節上我們可以推測出,SyncCache似乎是一個專門設計用來存儲含有SyncData的SyncCacheItem對象的緩存機制。從這個角度來看,SyncCache的功能和用途變得非常明顯,它就是一個用來加快數據訪問速度的臨時存儲區域,當需要訪問數據時,可以直接從SyncCache中獲取,從而提高程序的運行效率。同時,這也體現了設計者對于數據存儲和讀取效率的重視,通過引入緩存機制,使得頻繁訪問的數據能夠快速獲取,從而提升整體的性能表現。
4.2 Fast Cache(快速緩存)
圖8 快速存儲內部存儲結構
這里的快速緩存,其實和SyncCache在本質上是非常相似的,它們都是一種用于提升數據讀取速度的緩存機制。二者的主要區別在于,SyncCache是將數據存儲在一個列表中,而快速緩存則只是存儲了單個的SyncCacheItem。每個Item都通過兩個關鍵的Key來獲取其對應的data和lockCount。這種設計使得快速緩存能夠更快地讀取數據,因為它不需要遍歷整個列表,而是直接通過Key來獲取數據。同時,這也使得快速緩存的存儲空間更加節省,因為它不需要為一個列表分配大量的內存空間。
4.3 sDataLists的底層實現邏輯
sDataLists是一個全局性的靜態變量,意味著在整個應用程序中,它只能存在一個實例。在這個全局變量中,包含了一個名為SyncList的特殊列表。這個SyncList列表在程序中具有獨特的地位,它負責管理和同步所有需要共享和更新狀態的數據。由于它是靜態的,所以無論在程序的哪個部分,只要需要訪問sDataLists,都能直接通過它的名稱來引用它,而不需要先創建一個局部變量。這種設計使得數據的管理和同步變得更加高效和便捷。
圖9 sDataLists存儲結構
圖10 SyncList基本結構
4.4 StripedMap的設計與實現
在深入剖析sDataLists的源代碼架構時,我們可明確辨識出sDataLists本質上遵循哈希表的數據結構設計。這一精心策劃的架構背后,蘊含著深遠的目的與周詳的考量。哈希表作為一種高效的數據組織方式,其核心優勢在于顯著提升數據檢索的速度與效率。然而,在當前的實現框架中,哈希表所承載的功能與角色遠超于此單一范疇。
若我們摒棄采用StripedMap的策略,系統將不得不依賴單一的全局SyncList實例來統籌所有對象的鎖定與解鎖流程。此設計方案雖簡潔,卻潛藏著顯著的性能瓶頸。具體而言,每當有任一對象嘗試訪問或修改該哈希表時,其操作將被迫暫停,直至其他所有對象完成其解鎖操作,方可繼續執行。此類串行化的處理方式,無疑將大幅度加劇內存的占用情況,對系統整體性能構成不利影響。
StripedMap的引入,為現存問題提供了有效的解決方案。其核心價值體現在對單一的SyncList實施分片處理,這一機制確保了多個對象能夠并行且獨立地操作不同的SyncList實例。通過StripedMap預先配置并管理一定數量的SyncList,并在實際調用時采取均衡分配的策略,系統得以顯著提升其并發處理能力。具體而言,每個對象在執行加鎖操作時,均能夠自主選擇一個獨立的SyncList進行操作,從而成功規避了全局鎖可能引發的性能瓶頸問題。
4.5 TLS(Thread Local Storage)
TLS就是線程局部存儲,是操作系統為線程單獨提供的私有空間,能存儲只屬于當前線程的一些數據。
TLS,即線程局部存儲,是操作系統提供的一種機制,為每個線程單獨分配一塊私有內存空間,用于存儲只屬于該線程的數據。由于線程是操作系統進行任務調度和資源分配的最小單位,因此TLS可以確保每個線程擁有獨立的存儲空間,避免了線程之間的數據干擾和沖突,提高了程序的并發性和穩定性。
在多線程程序中,每個線程可能會訪問共享資源,如全局變量或共享內存區域,這會導致數據競爭和競態條件,從而影響程序的正確性和可靠性。為了解決這個問題,開發者通常需要使用同步機制,如互斥鎖、信號量等,來保護共享資源的訪問。但是這些同步機制會帶來額外的開銷,降低程序的性能。而使用TLS,可以將一些需要頻繁訪問且不涉及共享資源的數據存儲在當前線程的私有空間中,避免了同步機制的使用,提高了程序的運行效率和性能。
TLS的使用需要開發者在編寫程序時進行適當的聲明和初始化,以確保每個線程都能夠正確地訪問其私有空間中的數據。同時,由于TLS是操作系統提供的一種機制,其具體實現和接口可能會因操作系統的不同而有所差異,因此開發者需要根據具體的操作系統和編譯器環境進行相應的適配和調整。
4.6 id2Data的實現解析與研究
在探討我們當前的議題,現轉入id2Data的具體實現細節。鑒于源碼存在一定程度的冗余性,以下將依據4.6.1至4.6.4小節進行逐一解析。首先,系統將依據傳入的obj參數定位并檢索鎖及鏈表的起始節點。
圖11 id2Data基本結構之入參
4.6.1 FastCache快速查找
在每個線程的線程局部存儲(TLS)中,都會部署一個FastCache機制。該機制的實現過程主要包括以下幾個嚴謹且有序的步驟:
首先,系統會檢查是否存在SyncData數據。若存在,則立即將fastCacheOccupied標志位設置為YES,以明確指示快速緩存中已存有有效數據。
緊接著,系統將執行一項關鍵性檢查:驗證當前的SyncData對象是否與當前操作所針對的目標對象obj完全一致。若二者相符,則表明在快速緩存中已找到與目標對象相匹配的鎖數據。
在確認找到匹配鎖數據后,系統將讀取當前鎖的lockCount值,并依據當前傳入的操作類型(如加鎖或解鎖)來執行相應的操作。操作完成后,系統將更新后的lockCount值重新存儲回TLS中,以便在后續的查找操作中能夠迅速定位。
此FastCache機制的核心優勢在于,它通過在各線程的本地存儲中預先緩存鎖數據,有效提升了加鎖與解鎖操作的執行效率。此舉不僅避免了頻繁的全局范圍查找與更新操作,還顯著降低了鎖資源的爭用情況,從而實現了對并發性能的顯著提升。
圖12 id2Data基本結構之FastCache
4.6.2 SyncCache緩存遍歷查找
在FastCache快速查找機制未能成功定位目標時,程序將觸發fetch_cache函數的執行,以訪問當前線程的緩存數據,并繼續執行查找操作。值得注意的是,SyncCache本質上被設計為存儲SyncData元素的數組結構,這一過程實質上是遍歷數組以查找與當前操作對象obj相匹配的項。一旦找到匹配項,程序將根據傳入的操作類型執行相應的加鎖或解鎖操作,并同步更新lockCount的值。若lockCount遞減至0,則視為該線程已完成對該鎖的使用,隨即從線程緩存中移除該鎖實例。
為確保上述操作在多線程并發環境下的正確性和一致性,避免潛在的沖突問題,本機制采用OSAtomicDecrement32Barrier函數來原子性地減少result結構體中threadCount的值。此舉旨在確保在減少計數器的過程中,不會被其他線程的加鎖操作所干擾,從而維護了系統狀態的穩定與準確。
圖13 id2Data基本結構之SyncCache
4.6.3 sDataLists查找
若快速緩存與常規緩存均未檢索到目標項,則此線程首次執行@synchronized操作。在此情境下,系統轉而于sDataLists中檢索相應的SyncData對象。首先,通過執行lock操作對全局鏈表施以加鎖,此舉旨在防止多個線程并行創建同一對象的新鎖,確保線程安全。隨后,遍歷全局鏈表,以查找與當前對象相匹配的SyncData實例。
若遍歷過程中成功定位到匹配的SyncData,則將該實例賦值予result變量,并利用OSAtomicDecrement32Barrier原子操作遞增result->threadCount的值,此舉旨在防范與并發的釋放操作發生潛在沖突,確保數據一致性與線程安全。完成上述步驟后,該SyncData的相關信息即可在TLS(線程局部存儲)中被后續訪問。若系統檢測到存在未使用的SyncData實例,則優先考慮復用此類資源,以優化資源利用效率,減少不必要的對象創建與銷毀開銷。
圖14 id2Data基本結構之sDataLists
4.6.4 新建SyncData
若sDataLists中未能尋獲所需數據,則需自行構建SyncData并將其緩存至線程緩存,以備后續查詢之需。審視id2data的鎖獲取流程,其采用了一種類似于三級緩存的機制,即依次從快速緩存、常規緩存至哈希表進行檢索。此設計旨在高效管理多線程環境中的鎖資源,確保線程能迅速獲得鎖以執行加鎖與解鎖操作,從而提升系統效率。面對sDataLists檢索無果之情境,應采取相應策略,即創建SyncData的新實例,并將其妥善地納入緩存體系之中,以保障數據的完整性與系統的穩定運行。
圖15 id2Data基本結構之SyncData創建
圖16 id2Data基本結構之SyncData添加緩存
總結
圖17 id2Data三級緩存機制
通過本文上述自上而下的分析可以看出,@synchronized通過id2Data的三級緩存機制及時快速為傳入的對象obj拿到鎖,并清楚的記錄著這些鎖的lockCount及使用情況,確保在同一時間只有一個線程能夠執行,從而達到防止數據競爭和保證線程安全的目的。可以總結出以下幾點:
- @synchronized根據傳入的對象,為每個線程構建一把遞歸鎖,同時記錄每個線程加鎖的次數。基于此,對每條線程用不同的遞歸鎖進行加鎖和解鎖的操作,從而達到多線程遞歸調用的目的。
- @synchronized內部是基于os_unfair_lock封裝的遞歸互斥鎖,@synchronized在內部創建鎖的時候為了保證唯一性,使用spinlock_t來保證線程安全。
- @synchronized使用了快速緩存,線程緩存,全局鏈表方式來使得線程可以更加快速的拿到鎖,提升效率。
- 另外需要額外注意的是@synchronized使用時如果傳入nil,不能完成加鎖,使用時應避免。
總之,在iOS開發過程中,@synchronized提供了一種便捷的線程同步機制,使得開發者能夠輕松實現線程同步。尤其是在處理遞歸鎖的情況下,使用@synchronized能夠有效地避免死鎖的發生,大大提高了代碼的安全性和可靠性。這一特性對于開發者來說非常重要,因為它不僅確保了線程之間有序訪問共享資源,還降低了復雜度,使得開發者能夠更加專注于業務邏輯的實現。最后,本文探索iOS實現線程同步機制的過程,旨在同步系統開發領域為開發者提供具有參考價值的引導。
作者介紹
張璇,中國農業銀行股份有限公司研發中心軟件研發工程師,熟悉iOS開發,具備SpringBoot及React相關開發經驗,具備扎實的計算機基礎知識儲備。
張凱,中國農業銀行股份有限公司研發中心軟件研發工程師,擅長SpringBoot+Vue全棧式開發,熱愛編程和學習前沿技術信息。