Atomic原子操作原理剖析
前言
絕大部分 Objective-C
程式設計師使用屬性時,都不太關注一個特殊的修飾字首,一般都無腦的使用其非預設預設的狀態,他就是 atomic
。
@interface PropertyClass
@property (atomic, strong) NSObject *atomicObj; //預設也是atomic
@property (nonatomic, strong) NSObject *nonatomicObj;
@end
入門教程中一般都建議使用非原子操作,因為新手大部分操作都在主執行緒,用不到執行緒安全的特性,大量使用還會降低執行效率。
那他到底怎麼實現執行緒安全的呢?使用了哪種技術呢?
原理
屬性的實現
首先我們研究一下屬性包含的內容。通過查閱原始碼,其結構如下:
struct property_t {
const char *name; //名字
const char *attributes; //特性
};
屬性的結構比較簡單,包含了固定的名字和元素,可以通過 property_getName
獲取屬性名,property_getAttributes
獲取特性。
上例中 atomicObj
的特性為 [email protected]"NSObject",&,V_atomicObj
,其中 V
代表了 strong
,atomic
nonatomic
則顯示 N
。
那到底是怎麼實現原子操作的呢? 通過引入runtime
,我們能除錯一下呼叫的函式棧。
可以看到在編譯時就把屬性特性考慮進去了,Setter
方法直接呼叫了 objc_setProperty
的 atomic
版本。這裡不用 runtime
去動態分析特性,應該是對執行效能的考慮。
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) { //偏移為0說明改的是isa if (offset == 0) { object_setClass(self, newValue); return; } id oldValue; id *slot = (id*) ((char*)self + offset);//獲取原值 //根據特性拷貝 if (copy) { newValue = [newValue copyWithZone:nil]; } else if (mutableCopy) { newValue = [newValue mutableCopyWithZone:nil]; } else { if (*slot == newValue) return; newValue = objc_retain(newValue); } //判斷原子性 if (!atomic) { //非原子直接賦值 oldValue = *slot; *slot = newValue; } else { //原子操作使用自旋鎖 spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); } objc_release(oldValue); } id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) { // 取isa if (offset == 0) { return object_getClass(self); } // 非原子操作直接返回 id *slot = (id*) ((char*)self + offset); if (!atomic) return *slot; // 原子操作自旋鎖 spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); id value = objc_retain(*slot); slotlock.unlock(); // 出於效能考慮,在鎖之外autorelease return objc_autoreleaseReturnValue(value); }
什麼是自旋鎖呢?
鎖用於解決執行緒爭奪資源的問題,一般分為兩種,自旋鎖(spin)和互斥鎖(mutex)。
互斥鎖可以解釋為執行緒獲取鎖,發現鎖被佔用,就向系統申請鎖空閒時喚醒他並立刻休眠。
自旋鎖比較簡單,當執行緒發現鎖被佔用時,會不斷迴圈判斷鎖的狀態,直到獲取。
原子操作的顆粒度最小,只限於讀寫,對於效能的要求很高,如果使用了互斥鎖勢必在切換執行緒上耗費大量資源。相比之下,由於讀寫操作耗時比較小,能夠在一個時間片內完成,自旋更適合這個場景。
自旋鎖的坑
但是iOS 10之後,蘋果因為一個巨大的缺陷棄用了 OSSpinLock
改為新的 os_unfair_lock
。
新版 iOS 中,系統維護了 5 個不同的執行緒優先順序/QoS: background,utility,default,user-initiated,user-interactive。高優先順序執行緒始終會在低優先順序執行緒前執行,一個執行緒不會受到比它更低優先順序執行緒的干擾。這種執行緒排程演算法會產生潛在的優先順序反轉問題,從而破壞了 spin lock。
描述引用自 ibireme 大神的文章。
我的理解是,當低優先順序執行緒獲取了鎖,高優先順序執行緒訪問時陷入忙等狀態,由於是迴圈呼叫,所以佔用了系統排程資源,導致低優先順序執行緒遲遲不能處理資源並釋放鎖,導致陷入死鎖。
那為什麼原子操作用的還是 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
,實際測試兩者的效率差不多。
問答
atomic的實現機制
使用atomic
修飾屬性,編譯器會設定預設讀寫方法為原子讀寫,並使用互斥鎖新增保護。
為什麼不能保證絕對的執行緒安全?
單獨的原子操作絕對是執行緒安全的,但是組合一起的操作就不能保證。
- (void)competition {
self.intSource = 0;
dispatch_async(queue1, ^{
for (int i = 0; i < 10000; i++) {
self.intSource = self.intSource + 1;
}
});
dispatch_async(queue2, ^{
for (int i = 0; i < 10000; i++) {
self.intSource = self.intSource + 1;
}
});
}
最終得到的結果肯定小於20000。當獲取值的時候都是原子執行緒安全操作,比如兩個執行緒依序獲取了當前值 0
,於是分別增量後變為了 1
,所以兩個佇列依序寫入值都是 1
,所以不是執行緒安全的。
解決的辦法應該是增加顆粒度,將讀寫兩個操作合併為一個原子操作,從而解決寫入過期資料的問題。
os_unfair_lock_t unfairLock;
- (void)competition {
self.intSource = 0;
unfairLock = &(OS_UNFAIR_LOCK_INIT);
dispatch_async(queue1, ^{
for (int i = 0; i < 10000; i++) {
os_unfair_lock_lock(unfairLock);
self.intSource = self.intSource + 1;
os_unfair_lock_unlock(unfairLock);
}
});
dispatch_async(queue2, ^{
for (int i = 0; i < 10000; i++) {
os_unfair_lock_lock(unfairLock);
self.intSource = self.intSource + 1;
os_unfair_lock_unlock(unfairLock);
}
});
}
總結
通過學習屬性的原子性,對系統中鎖的理解又加深,包括自旋鎖,互斥鎖,讀寫鎖等。
本來都以為實現是自旋鎖了,還好留了個心眼多看了一層才發現最終實現還是互斥鎖。這件事也給我一個小教訓,查閱原始碼還是要刨根問底,只浮於表面的話,可能得不到想要的真相。