iOS 底層探索:常見的鎖

iOS 底層探索: 學習大綱 OC篇

前言

  • 上一篇講了@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方法,其中就有atomicnonatomic操作

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是大致相同的
image.png
  • 通過源碼實現可以看出,底層是通過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遞歸特性。內部任務是遞歸持有的,所以不會死鎖

image

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是對mutexcond的一種封裝(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?
}

通過源碼可以看出

  • NSConditionLockNSCondition的封裝

  • 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的存儲,通過鏈表表示可重入(即嵌套)的特性,雖然性能較低,但由于簡單好用,使用頻率很高

  • NSLockNSRecursiveLock底層是對pthread_mutex的封裝

  • NSConditionNSConditionLock是條件鎖,底層都是對pthread_mutex的封裝,當滿足某一個條件時才能進行操作,和信號量dispatch_semaphore類似

五、 鎖的使用場景

  • 如果只是簡單的使用,例如涉及線程安全,使用NSLock即可

  • 如果是循環嵌套,推薦使用@synchronized,主要是因為使用遞歸鎖的性能不如使用@synchronized的性能(因為在synchronized中無論怎么重入,都沒有關系,而NSRecursiveLock可能會出現崩潰現象)

  • 循環嵌套中,如果對遞歸鎖掌握的很好,則建議使用遞歸鎖,因為性能好

  • 如果是循環嵌套,并且還有多線程影響時,例如有等待、死鎖現象時,建議使用@synchronized

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容