iOS-atomic修飾符原理剖析講解 (你將會了解到什么是優先級翻轉、自旋鎖、互斥鎖)


 
 
前言
 
這里面你將會了解到什么是 優先級翻轉、自旋鎖、互斥鎖
絕大部分 Objective-C 程序員使用屬性時,都不太關注一個特殊的修飾前綴,一般都無腦的使用其非默認缺省的狀態,他就是 atomic。
1 @interface PropertyClass
2  
3 @property (atomic, strong) NSObject *atomicObj; //缺省也是atomic
4 @property (nonatomic, strong) NSObject *nonatomicObj;
5  
6 @end

 

入門教程中一般都建議使用非原子操作,因為新手大部分操作都在主線程,用不到線程安全的特性,大量使用還會降低執行效率。
那他到底怎么實現線程安全的呢?使用了哪種技術呢?

原理

屬性的實現

首先我們研究一下屬性包含的內容。通過查閱源碼,其結構如下:
1 struct property_t {
2 const char *name; //名字
3 const char *attributes; //特性
4 };

 

屬性的結構比較簡單,包含了固定的名字和元素,可以通過 property_getName 獲取屬性名,property_getAttributes 獲取特性。
上例中 atomicObj 的特性為 T@"NSObject",&,V_atomicObj,其中 V 代表了 strong,atomic 特性缺省沒有顯示,如果是 nonatomic 則顯示 N。
那到底是怎么實現原子操作的呢? 通過引入runtime,我們能調試一下調用的函數棧。

 

 

 

可以看到在編譯時就把屬性特性考慮進去了,Setter 方法直接調用了 objc_setProperty 的 atomic 版本。這里不用 runtime 去動態分析特性,應該是對執行性能的考慮。
 
 1 static inline void reallySetProperty(id self, SEL _cmd,
 2 id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) {
 3 //偏移為0說明改的是isa
 4 if (offset == 0) {
 5 object_setClass(self, newValue);
 6 return;
 7 }
 8  
 9 id oldValue;
10 id *slot = (id*) ((char*)self + offset);//獲取原值
11 //根據特性拷貝
12 if (copy) {
13 newValue = [newValue copyWithZone:nil];
14 } else if (mutableCopy) {
15 newValue = [newValue mutableCopyWithZone:nil];
16 } else {
17 if (*slot == newValue) return;
18 newValue = objc_retain(newValue);
19 }
20 //判斷原子性
21 if (!atomic) {
22 //非原子直接賦值
23 oldValue = *slot;
24 *slot = newValue;
25 } else {
26 //原子操作使用自旋鎖
27 spinlock_t& slotlock = PropertyLocks[slot];
28 slotlock.lock();
29 oldValue = *slot;
30 *slot = newValue;
31 slotlock.unlock();
32 }
33  
34 objc_release(oldValue);
35 }
36  
37 id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
38 // 取isa
39 if (offset == 0) {
40 return object_getClass(self);
41 }
42  
43 // 非原子操作直接返回
44 id *slot = (id*) ((char*)self + offset);
45 if (!atomic) return *slot;
46 // 原子操作自旋鎖
47 spinlock_t& slotlock = PropertyLocks[slot];
48 slotlock.lock();
49 id value = objc_retain(*slot);
50 slotlock.unlock();
51 // 出於性能考慮,在鎖之外autorelease
52 return objc_autoreleaseReturnValue(value);
53 }

 

什么是自旋鎖呢?

鎖用於解決線程爭奪資源的問題,一般分為兩種,自旋鎖(spin)和互斥鎖(mutex)。
互斥鎖可以解釋為線程獲取鎖,發現鎖被占用,就向系統申請鎖空閑時喚醒他並立刻休眠。互斥鎖加鎖的時候,等待鎖的線程處於休眠狀態,不會占用CPU的資源
自旋鎖比較簡單,當線程發現鎖被占用時,會不斷循環判斷鎖的狀態,直到獲取。自旋鎖加鎖的時候,等待鎖的線程處於忙等狀態,並且占用着CPU的資源。
原子操作的顆粒度最小,只限於讀寫,對於性能的要求很高,如果使用了互斥鎖勢必在切換線程上耗費大量資源。相比之下,由於讀寫操作耗時比較小,能夠在一個時間片內完成,自旋更適合這個場景。

自旋鎖的坑

但是iOS 10之后,蘋果因為一個巨大的缺陷棄用了 OSSpinLock 改為新的 os_unfair_lock。
新版 iOS 中,系統維護了 5 個不同的線程優先級/QoS: background,utility,default,user-initiated,user-interactive。高優先級線程始終會在低優先級線程前執行,一個線程不會受到比它更低優先級線程的干擾。這種線程調度算法會產生潛在的優先級反轉問題,從而破壞了 spin lock。
描述引用自 ibireme 大神的文章。
優先級翻轉的問題
新版 iOS 中,系統維護了 5 個不同的線程優先級/QoS: background,utility,default,user-initiated,user-interactive。高優先級線程始終會在低優先級線程前執行,一個線程不會受到比它更低優先級線程的干擾。這種線程調度算法會產生潛在的優先級反轉問題,從而破壞了 spin lock。
具體來說,如果一個低優先級的線程獲得鎖並訪問共享資源,這時一個高優先級的線程也嘗試獲得這個鎖,它會處於 spin lock 的忙等狀態從而占用大量 CPU。此時低優先級線程無法與高優先級線程爭奪 CPU 時間,從而導致任務遲遲完不成、無法釋放 lock。導致陷入死鎖
這並不只是理論上的問題,libobjc 已經遇到了很多次這個問題了,於是蘋果的工程師停用了 OSSpinLock。iOS10以后,蘋果給出了新的api
那為什么原子操作用的還是 spinlock_t 呢?
 
using spinlock_t = mutex_tt<LOCKDEBUG>;
using mutex_t = mutex_tt<LOCKDEBUG>;
 
class mutex_tt : nocopy_t {
os_unfair_lock mLock; //處理了優先級的互斥鎖
void lock() {
lockdebug_mutex_lock(this);
os_unfair_lock_lock_with_options_inline
(&mLock, OS_UNFAIR_LOCK_DATA_SYNCHRONIZATION);
}
void unlock() {
lockdebug_mutex_unlock(this);
os_unfair_lock_unlock_inline(&mLock);
}
}

 

差點被蘋果騙了!原來系統中自旋鎖已經全部改為互斥鎖實現了,只是名稱一直沒有更改。
為了修復優先級反轉的問題,蘋果也只能放棄使用自旋鎖,改用優化了性能的 os_unfair_lock,實際測試兩者的效率差不多。
os_unfair_lock用於取代不安全的OSSpinLock,從iOS10開始才支持
從底層調用看,等待os_unfair_lock鎖的線程會處於休眠狀態,並非忙等 

問答

atomic的實現機制

使用atomic 修飾屬性,編譯器會設置默認讀寫方法為原子讀寫,並使用互斥鎖添加保護。

為什么不能保證絕對的線程安全?

單獨的原子操作絕對是線程安全的,但是組合一起的操作就不能保證。
 
 1 - (void)competition {
 2 self.intSource = 0;
 3  
 4 dispatch_async(queue1, ^{
 5 for (int i = 0; i < 10000; i++) {
 6 self.intSource = self.intSource + 1;
 7 }
 8 });
 9  
10 dispatch_async(queue2, ^{
11 for (int i = 0; i < 10000; i++) {
12 self.intSource = self.intSource + 1;
13 }
14 });
15 }

 

最終得到的結果肯定小於20000。當獲取值的時候都是原子線程安全操作,比如兩個線程依序獲取了當前值 0,於是分別增量后變為了 1,所以兩個隊列依序寫入值都是 1,所以不是線程安全的。
解決的辦法應該是增加顆粒度,將讀寫兩個操作合並為一個原子操作,從而解決寫入過期數據的問題。
 
 1 os_unfair_lock_t unfairLock;
 2 - (void)competition {
 3 self.intSource = 0;
 4  
 5 unfairLock = &(OS_UNFAIR_LOCK_INIT);
 6 dispatch_async(queue1, ^{
 7 for (int i = 0; i < 10000; i++) {
 8 os_unfair_lock_lock(unfairLock);
 9 self.intSource = self.intSource + 1;
10 os_unfair_lock_unlock(unfairLock);
11 }
12 });
13  
14 dispatch_async(queue2, ^{
15 for (int i = 0; i < 10000; i++) {
16 os_unfair_lock_lock(unfairLock);
17 self.intSource = self.intSource + 1;
18 os_unfair_lock_unlock(unfairLock);
19 }
20 });
21 }

 


總結

通過學習屬性的原子性,對系統中鎖的理解又加深,包括自旋鎖,互斥鎖,讀寫鎖等。
本來都以為實現是自旋鎖了,還好留了個心眼多看了一層才發現最終實現還是互斥鎖。這件事也給我一個小教訓,查閱源碼還是要刨根問底,只浮於表面的話,可能得不到想要的真相。

引用

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM