絕大部分 Objective-C
程序員使用屬性時,都不太關注一個特殊的修飾前綴,通常都無腦的使用其非默認缺省的狀態,他就是 atomic
。ios
@interface PropertyClass @property (atomic, strong) NSObject *atomicObj; //缺省也是atomic @property (nonatomic, strong) NSObject *nonatomicObj; @end 複製代碼
入門教程中通常都建議使用非原子操做,由於新手大部分操做都在主線程,用不到線程安全的特性,大量使用還會下降執行效率。git
那他到底怎麼實現線程安全的呢?使用了哪一種技術呢?程序員
首先咱們研究一下屬性包含的內容。經過查閱源碼,其結構以下:github
struct property_t { const char *name; //名字 const char *attributes; //特性 }; 複製代碼
屬性的結構比較簡單,包含了固定的名字和元素,能夠經過 property_getName
獲取屬性名,property_getAttributes
獲取特性。算法
上例中 atomicObj
的特性爲 T@"NSObject",&,V_atomicObj
,其中 V
表明了 strong
,atomic
特性缺省沒有顯示,若是是 nonatomic
則顯示 N
。安全
那究竟是怎麼實現原子操做的呢? 經過引入runtime
,咱們能調試一下調用的函數棧。markdown
能夠看到在編譯時就把屬性特性考慮進去了,Setter
方法直接調用了 objc_setProperty
的 atomic
版本。這裏不用 runtime
去動態分析特性,應該是對執行性能的考慮。async
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)。函數
互斥鎖能夠解釋爲線程獲取鎖,發現鎖被佔用,就向系統申請鎖空閒時喚醒他並馬上休眠。oop
自旋鎖比較簡單,當線程發現鎖被佔用時,會不斷循環判斷鎖的狀態,直到獲取。
原子操做的顆粒度最小,只限於讀寫,對於性能的要求很高,若是使用了互斥鎖勢必在切換線程上耗費大量資源。相比之下,因爲讀寫操做耗時比較小,可以在一個時間片內完成,自旋更適合這個場景。
可是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
修飾屬性,編譯器會設置默認讀寫方法爲原子讀寫,並使用互斥鎖添加保護。
單獨的原子操做絕對是線程安全的,可是組合一塊兒的操做就不能保證。
- (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); } }); } 複製代碼
經過學習屬性的原子性,對系統中鎖的理解又加深,包括自旋鎖,互斥鎖,讀寫鎖等。
原本都覺得實現是自旋鎖了,還好留了個心眼多看了一層才發現最終實現仍是互斥鎖。這件事也給我一個小教訓,查閱源碼仍是要刨根問底,只浮於表面的話,可能得不到想要的真相。