前言
- 上一篇講了@Synchronized 這個互斥遞歸鎖的底層的原理,今天來拓展一下iOS中其他鎖的一些原理,然后進行對比分析。
一、 常見鎖的性能對比
可以看出,圖中鎖的性能從高到底依次是:
OSSpinLock(自旋鎖) :性能最高
synchronized(互斥鎖):性能最低
二、 鎖的分類
互斥鎖
它將代碼切片成為一個個代碼塊,使得當一個代碼塊在運行時,其他線程不能運行他們之中的任意片段,只有等到該片段結束運行后才可以運行。通過這種方式來防止多個線程同時對某一資源進行讀寫的一種機制。常用的有:
- @synchronized
- NSLock
- pthread_mutex
自旋鎖
多線程同步的一種機制,當其檢測到資源不可用時,會保持一種“忙等”的狀態,直到獲取該資源。它的優勢在于避免了上下文的切換,非常適合于堵塞時間很短的場合;缺點則是在“忙等”的狀態下會不停的檢測狀態,會占用 cpu 資源。常用的有:
- OSSpinLock
- atomic
條件鎖
通過一些條件來控制資源的訪問,當然條件是會發生變化的。常用的有:
- NSCondition
- NSConditionLock
信號量
是一種高級的同步機制。互斥鎖可以認為是信號量取值0/1時的特例,可以實現更加復雜的同步。常用的有:
- dispatch_semaphore
遞歸鎖
它允許同一線程多次加鎖,而不會造成死鎖。遞歸鎖是特殊的互斥鎖,主要是用在循環或遞歸操作中。常用的有:
- pthread_mutex(recursive)
- NSRecursiveLock
讀寫鎖
是并發控制的一種同步機制,也稱“共享-互斥鎖”,也是一種特殊的自旋鎖。它把對資源的訪問者分為讀者和寫者,它允許同時有多個讀者訪問資源,但是只允許有一個寫者來訪問資源。常用的有:
- pthread(rwlock)
- dispatch_barrier_async / dispatch_barrier_sync
三、常見幾種鎖的使用方法及底層原理
3.1、atomic(原子鎖)
atomic
適用于OC中屬性的修飾符,其自帶一把自旋鎖,但是這個一般基本不使用,都是使用的nonatomic
我們知道setter
方法會根據修飾符調用不同方法,其中最后會統一調用reallySetProperty
方法,其中就有atomic
和nonatomic
操作
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
...
id *slot = (id*) ((char*)self + offset);
...
if (!atomic) {//未加鎖
oldValue = *slot;
*slot = newValue;
} else {//加鎖
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
...
}
從源碼中可以看出,對于atomic
修飾的屬性,進行了spinlock_t
加鎖處理,但是OSSpinLock
已經廢棄了,這里的spinlock_t
在底層是通過os_unfair_lock
替代了OSSpinLock
實現的加鎖。同時為了防止哈希沖突
,還是用了加鹽
操作
using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
...
}
getter
方法中對atomic
的處理,同setter
是大致相同的
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();//加鎖
id value = objc_retain(*slot);
slotlock.unlock();//解鎖
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
3.2、synchronized(互斥遞歸鎖)
已分析過,請查看iOS 底層探索:Dispatch_source & @Synchronized
3.3、NSLock
NSLock
是對下層pthread_mutex的封裝
,使用如下
NSLock *lock = [[NSLock alloc] init];
[lock lock];
[lock unlock];
直接進入NSLock
定義查看,其遵循了NSLocking
協議,下面來探索NSLock的底層實現
3.3.1 NSLock 底層分析
- NSLock源碼在
Foundation
框架中, 由于OC的Foundation
框架不開源,所以這里參考Swift
的開源框架Foundation
來 分析NSLock
的底層實現,其原理與OC是大致相同的
- 通過源碼實現可以看出,底層是通過
pthread_mutex
互斥鎖實現的。并且在init方法中,還做了一些其他操作,所以在使用NSLock時需要使用init初始化
回到前文的性能圖中,可以看出NSLock
的性能僅次于 pthread_mutex
(互斥鎖),非常接近
3.3.2 使用弊端
請問下面block嵌套block的代碼中,會有什么問題?
for (int i= 0; i<100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
testMethod(10);
});
}
在未加鎖之前,其中的current=9、10有很多條,導致數據混亂,主要原因是多線程導致的
如果像下面這樣加鎖,會有什么問題?
NSLock *lock = [[NSLock alloc] init];
for (int i= 0; i<100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
testMethod(10);
[lock unlock];
});
}
其運行結果如下
會出現一直等待的情況,主要是因為嵌套使用的遞歸
,使用NSLock(簡單的互斥鎖,如果沒有回來,會一直睡覺等待)
,即會存在一直加lock,等不到unlock 的堵塞情況
所以,針對這種情況,可以使用以下方式解決
- 使用
@synchronized
for (int i= 0; i<100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
@synchronized (self) {
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
}
};
testMethod(10);
});
}
- 使用遞歸鎖
NSRecursiveLock
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
for (int i= 0; i<100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
[recursiveLock lock];
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
[recursiveLock unlock];
};
testMethod(10);
});
}
3.4 pthread_mutex
pthread_mutex
就是互斥鎖本身
,當鎖被占用,其他線程申請鎖時,不會一直忙等待,而是阻塞線程并睡眠。
使用
// 導入頭文件
#import <pthread.h>
// 全局聲明互斥鎖
pthread_mutex_t _lock;
// 初始化互斥鎖
pthread_mutex_init(&_lock, NULL);
// 加鎖
pthread_mutex_lock(&_lock);
// 這里做需要線程安全操作
// 解鎖
pthread_mutex_unlock(&_lock);
// 釋放鎖
pthread_mutex_destroy(&_lock);
3.5、NSRecursiveLock
-
在
聲明前加鎖
和調用后解鎖
是正確的。image -
但由于它具備
遞歸特性
,我們在block內部
的遞歸前
加鎖
,當前線程
也打印正常
,但是其他
線程堵塞
。image -
當我們去掉
for循環
,僅保持一個異步線程
,在block內部
的遞歸前后
分別加鎖
和解鎖
,打印正常:image
這是因為NSRecursiveLock
的遞歸特性
。內部任務是遞歸持有
的,所以不會死鎖
。
3.6、NSCondition
NSCondition
是一個條件鎖
,在日常開發中使用較少,與信號量有點相似:線程1需要滿足條件1才會往下走,否則會堵塞等待,知道條件滿足。經典模型是生產消費者模型
NSCondition的對象
實際上作為一個鎖
和 一個線程檢查器
鎖
主要 為了當檢測條件時保護數據源,執行條件引發的任務
線程檢查器
主要是根據條件決定是否繼續運行線程
,即線程是否被阻塞
使用
//初始化
NSCondition *condition = [[NSCondition alloc] init]
//一般用于多線程同時訪問、修改同一個數據源,保證在同一 時間內數據源只被訪問、修改一次,其他線程的命令需要在lock 外等待,只到 unlock ,才可訪問
[condition lock];
//與lock 同時使用
[condition unlock];
//讓當前線程處于等待狀態
[condition wait];
//CPU發信號告訴線程不用在等待,可以繼續執行
[condition signal];
通過swift的Foundation源碼查看NSCondition
的底層實現
open class NSCondition: NSObject, NSLocking {
internal var mutex = _MutexPointer.allocate(capacity: 1)
internal var cond = _ConditionVariablePointer.allocate(capacity: 1)
//初始化
public override init() {
pthread_mutex_init(mutex, nil)
pthread_cond_init(cond, nil)
}
//析構
deinit {
pthread_mutex_destroy(mutex)
pthread_cond_destroy(cond)
mutex.deinitialize(count: 1)
cond.deinitialize(count: 1)
mutex.deallocate()
cond.deallocate()
}
//加鎖
open func lock() {
pthread_mutex_lock(mutex)
}
//解鎖
open func unlock() {
pthread_mutex_unlock(mutex)
}
//等待
open func wait() {
pthread_cond_wait(cond, mutex)
}
//等待
open func wait(until limit: Date) -> Bool {
guard var timeout = timeSpecFrom(date: limit) else {
return false
}
return pthread_cond_timedwait(cond, mutex, &timeout) == 0
}
//信號,表示等待的可以執行了
open func signal() {
pthread_cond_signal(cond)
}
//廣播
open func broadcast() {
// 匯編分析 - 猜 (多看多玩)
pthread_cond_broadcast(cond) // wait signal
}
open var name: String?
}
其底層也是對下層pthread_mutex
的封裝
NSCondition
是對mutex
和cond
的一種封裝(cond就是用于訪問和操作特定類型數據的指針)wait
操作會阻塞線程
,使其進入休眠狀態
,直至超時signal
操作是喚醒
一個正在休眠等待的線程broadcast
會喚醒所有正在等待的線程
3.6、NSConditionLock
NSConditionLock
是條件鎖,一旦一個線程獲得鎖,其他線程一定等待
相比NSConditionLock
而言,NSCondition
使用比較麻煩,所以推薦使用NSConditionLock
,其使用如下
//初始化
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
//表示 conditionLock 期待獲得鎖,如果沒有其他線程獲得鎖(不需要判斷內部的 condition) 那它能執行此行以下代碼,如果已經有其他線程獲得鎖(可能是條件鎖,或者無條件 鎖),則等待,直至其他線程解鎖
[conditionLock lock];
//表示如果沒有其他線程獲得該鎖,但是該鎖內部的 condition不等于A條件,它依然不能獲得鎖,仍然等待。如果內部的condition等于A條件,并且 沒有其他線程獲得該鎖,則進入代碼區,同時設置它獲得該鎖,其他任何線程都將等待它代碼的 完成,直至它解鎖。
[conditionLock lockWhenCondition:A條件];
//表示釋放鎖,同時把內部的condition設置為A條件
[conditionLock unlockWithCondition:A條件];
// 表示如果被鎖定(沒獲得 鎖),并超過該時間則不再阻塞線程。但是注意:返回的值是NO,它沒有改變鎖的狀態,這個函 數的目的在于可以實現兩種狀態下的處理
return = [conditionLock lockWhenCondition:A條件 beforeDate:A時間];
//其中所謂的condition就是整數,內部通過整數比較條件
NSConditionLock
,其本質就是NSCondition + Lock
,以下是其swift的底層實現,
open class NSConditionLock : NSObject, NSLocking {
internal var _cond = NSCondition()
internal var _value: Int
internal var _thread: _swift_CFThreadRef?
public convenience override init() {
self.init(condition: 0)
}
public init(condition: Int) {
_value = condition
}
open func lock() {
let _ = lock(before: Date.distantFuture)
}
open func unlock() {
_cond.lock()
_thread = nil
_cond.broadcast()
_cond.unlock()
}
open var condition: Int {
return _value
}
open func lock(whenCondition condition: Int) {
let _ = lock(whenCondition: condition, before: Date.distantFuture)
}
open func `try`() -> Bool {
return lock(before: Date.distantPast)
}
open func tryLock(whenCondition condition: Int) -> Bool {
return lock(whenCondition: condition, before: Date.distantPast)
}
open func unlock(withCondition condition: Int) {
_cond.lock()
_thread = nil
_value = condition
_cond.broadcast()
_cond.unlock()
}
open func lock(before limit: Date) -> Bool {
_cond.lock()
while _thread != nil {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
_thread = pthread_self()
_cond.unlock()
return true
}
open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
_cond.lock()
while _thread != nil || _value != condition {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
_thread = pthread_self()
_cond.unlock()
return true
}
open var name: String?
}
通過源碼可以看出
NSConditionLock
是NSCondition
的封裝NSConditionLock
可以設置鎖條件
,即condition值,而NSCondition
只是信號的通知
四、性能總結
OSSpinLock自旋鎖
由于安全性問題,在iOS10之后已經被廢棄,其底層的實現用os_unfair_lock
替代
使用
OSSpinLock
及所示,會處于忙等待狀態
而
os_unfair_lock
是處于休眠狀態
atomic原子鎖
自帶一把自旋鎖,只能保證setter、getter
時的線程安全,在日常開發中使用更多的還是nonatomic
修飾屬性
atomic
:當屬性在調用setter、getter
方法時,會加上自旋鎖osspinlock
,用于保證同一時刻只能有一個線程調用屬性的讀或寫,避免了屬性讀寫不同步的問題
。由于是底層編譯器自動生成的互斥鎖代碼,會導致效率相對較低
nonatomic
:當屬性在調用setter、getter方法時,不會加上自旋鎖,即線程不安全
。由于編譯器不會自動生成互斥鎖代碼,可以提高效率
@synchronized
在底層維護了一個哈希表
進行線程data的存儲,通過鏈表
表示可重入
(即嵌套)的特性,雖然性能較低,但由于簡單好用,使用頻率很高
NSLock
、NSRecursiveLock
底層是對pthread_mutex
的封裝
NSCondition
和NSConditionLock
是條件鎖,底層都是對pthread_mutex
的封裝,當滿足某一個條件時才能進行操作,和信號量dispatch_semaphore
類似
五、 鎖的使用場景
如果只是
簡單
的使用,例如涉及線程安全,使用NSLock
即可如果是
循環嵌套
,推薦使用@synchronized
,主要是因為使用遞歸鎖
的性能不如使用@synchronized
的性能(因為在synchronized
中無論怎么重入,都沒有關系,而NSRecursiveLock
可能會出現崩潰現象)在
循環嵌套
中,如果對遞歸鎖掌握的很好,則建議使用遞歸鎖
,因為性能好如果是
循環嵌套
,并且還有多線程影響
時,例如有等待、死鎖現象時,建議使用@synchronized