27.iOS底層學(xué)習(xí)之八大鎖的分析

本章提綱:
1、NSLock
2、NSRecursiveLock
3、NSCondition
4、NSConditionLock
5、讀寫(xiě)鎖的實(shí)現(xiàn)

上一篇我們了解了synchronized的使用,這篇文章來(lái)補(bǔ)充其他我們平時(shí)開(kāi)發(fā)中常用的一些鎖的示例,以及簡(jiǎn)單的源碼探索。

1.NSLock

  • NSLock的基本使用
//NSLock的創(chuàng)建
 NSLock *nlock = [[NSLock alloc] init];

//NSLock加鎖
[nlock lock];

// NSLock解鎖
 [nlock unlock];

它的使用非常簡(jiǎn)單,下面我們來(lái)結(jié)合具體示例。

1.1 NSLock的使用示例
  • 測(cè)試代碼
    多線(xiàn)程遞歸打印,未加鎖之前
    未加鎖.png

    可以看到打印結(jié)果,在未經(jīng)過(guò)任何臨界區(qū)處理,沒(méi)有加鎖的情況下,打印的結(jié)果非常隨意混亂。

多線(xiàn)程遞歸打印,遞歸外部加鎖NSLock

遞歸外部加鎖NSLock

可以看到結(jié)果變得開(kāi)始有順序了,在一次線(xiàn)程操作未處理完畢之前,下一個(gè)線(xiàn)程不能訪(fǎng)問(wèn),所以每次線(xiàn)程執(zhí)行遞歸的,都是有序的打印。

多線(xiàn)程遞歸打印,遞歸內(nèi)部加鎖NSLock

遞歸內(nèi)部加鎖NSLock

可以看到,只打印了10,程序就沒(méi)有后續(xù)了,在遞歸內(nèi)部,加了一次鎖之后,testMethod又調(diào)用了自己,然后又走到[lock lock];因?yàn)檫@段已經(jīng)加了鎖,所以要等著解鎖才能再一次執(zhí)行testMethod,因?yàn)榻怄i的代碼在testMethod之后,永遠(yuǎn)也走不到,所以這里有點(diǎn)死鎖的狀態(tài)。

通過(guò)上面三種情況的討論,可以了解到NSLock可以用于多線(xiàn)程使線(xiàn)程安全,起到保護(hù)臨界區(qū)的作用。但是不能在遞歸中使用。我們后邊要學(xué)習(xí)的NSRecursiveLock是可以在遞歸中使用的,后面詳細(xì)介紹。

1.2 NSLock源碼窺探

我們打一個(gè)符號(hào)斷點(diǎn)-[NSLock lock],看看NSLock的底層實(shí)現(xiàn)所在的框架。

NSLock符號(hào)斷點(diǎn)

可以看到NSLock是出自于Foundation框架,而oc版本的Foundation框架并沒(méi)有開(kāi)源,我們通過(guò)Swift版本的來(lái)看一下里面的具體實(shí)現(xiàn),打開(kāi)swift-corelibs-foundation-master代碼,搜索NSLock,來(lái)到它的定義的部分。

NSLock源碼定義

1、我們通過(guò)這個(gè)源碼可以了解到NSLock遵循NSLocking,而NSLocking是協(xié)議。
2、而NSLock的初始化init實(shí)際上調(diào)用的是pthread_mutex_initpthread_mutex_initpthread框架下的一個(gè)API,所以NSLock的實(shí)際實(shí)現(xiàn)其實(shí)是對(duì)pthread的封裝。

  • pthread簡(jiǎn)介
    pthread是 POSIX threads 的簡(jiǎn)稱(chēng),是POSIX的線(xiàn)程標(biāo)準(zhǔn)。POSIX是可移植操作系統(tǒng)接口(Portable Operating System Interface)的簡(jiǎn)稱(chēng),其定義了操作系統(tǒng)的標(biāo)準(zhǔn)接口,旨在獲得源代碼級(jí)別的軟件可移植性。它是一套通用的多線(xiàn)程API,適用于Unix、Linux、Windows等系統(tǒng),跨平臺(tái)、可移植,使用難度大,C語(yǔ)言框架,線(xiàn)程生命周期由程序員管理。

從源碼的部分點(diǎn)進(jìn)去,可以看到一部分pthread的API,有初始化的,加鎖,解鎖的等等。


pthread

2. NSRecursiveLock

同樣NSRecursiveLock也是對(duì)pthread的封裝,具體的實(shí)現(xiàn)和NSLock的差別我們?cè)谙旅嬖创a的時(shí)候說(shuō)明。先來(lái)看它和NSLock在使用上的差別。還是上面的代碼做示例。

2.1 NSRecursiveLock的使用示例

多線(xiàn)程遞歸打印,遞歸外部加鎖NSRecursiveLock

遞歸外部加鎖NSRecursiveLock

它的效果和NSLock是一樣的。

多線(xiàn)程遞歸打印,遞歸內(nèi)部加鎖NSRecursiveLock

遞歸內(nèi)部加鎖NSRecursiveLock

可以看到,一次的遞歸調(diào)用完成了,打印出了完整的10到1。我具體的打了個(gè)斷點(diǎn),看看這個(gè)遞歸鎖在遞歸中是怎么執(zhí)行的,
具體調(diào)試

左邊線(xiàn)程堆棧展示的invoke2比十個(gè)多,但是我們遞歸一共調(diào)用了十次,也就是說(shuō)別的線(xiàn)程有的調(diào)用了lock也記到了當(dāng)前線(xiàn)程里邊去,所以在解鎖的時(shí)候當(dāng)前線(xiàn)程多調(diào)用了unlock,所以引發(fā)了崩潰。

通過(guò)這個(gè)表現(xiàn)我們可以看出來(lái)NSRecursiveLock支持遞歸使用,但是不支持多線(xiàn)程!

2.2 NSRecursiveLock源碼窺探

我們來(lái)到源碼看到NSRecursiveLock的實(shí)現(xiàn)和NSLock很相似。

image.png

多了個(gè)attr的初始化和設(shè)置,傳的參數(shù)是PTHREAD_MUTEX_RECURSIVE

 pthread_mutexattr_init(attrs)
 pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))

而且在pthread_mutex_init()調(diào)用時(shí),NSLock傳的是nil,而NSRecursiveLock把初始化好的attrs傳了進(jìn)去。

3. NSCondition

NSCondition是條件鎖,它的使用方式和信號(hào)量很相似,當(dāng)線(xiàn)程符合條件的時(shí)候才會(huì)執(zhí)行,否則會(huì)等待被阻塞,等待滿(mǎn)足條件時(shí)再執(zhí)行。

  • API展示
//加鎖
[condition lock];

//與lock同時(shí)使?,解鎖
[condition unlock];

//使當(dāng)前線(xiàn)程處于等待狀態(tài)
[condition wait];

//CPU發(fā)信號(hào)告訴線(xiàn)程不?等待,繼續(xù)執(zhí)?
[condition signal];
3.1 NSCondition的使用示例
3.1.1生產(chǎn)者消費(fèi)者問(wèn)題簡(jiǎn)介

生產(chǎn)者消費(fèi)者問(wèn)題(英語(yǔ):Producer-consumer problem),也稱(chēng)有限緩沖問(wèn)題(英語(yǔ):Bounded-buffer problem),是一個(gè)多線(xiàn)程同步問(wèn)題的經(jīng)典案例。該問(wèn)題描述了兩個(gè)共享固定大小緩沖區(qū)線(xiàn)程——即所謂的“生產(chǎn)者”和“消費(fèi)者”——在實(shí)際運(yùn)行時(shí)會(huì)發(fā)生的問(wèn)題。生產(chǎn)者的主要作用是生成一定量的數(shù)據(jù)放到緩沖區(qū)中,然后重復(fù)此過(guò)程。與此同時(shí),消費(fèi)者也在緩沖區(qū)消耗這些數(shù)據(jù)。該問(wèn)題的關(guān)鍵就是要保證生產(chǎn)者不會(huì)在緩沖區(qū)滿(mǎn)時(shí)加入數(shù)據(jù),消費(fèi)者也不會(huì)在緩沖區(qū)中空時(shí)消耗數(shù)據(jù)。

3.1.2解決這類(lèi)問(wèn)題的辦法

要解決該問(wèn)題,就必須讓生產(chǎn)者在緩沖區(qū)滿(mǎn)時(shí)休眠(要么干脆就放棄數(shù)據(jù)),等到下次消費(fèi)者消耗緩沖區(qū)中的數(shù)據(jù)的時(shí)候,生產(chǎn)者才能被喚醒,開(kāi)始往緩沖區(qū)添加數(shù)據(jù)。同樣,也可以讓消費(fèi)者在緩沖區(qū)空時(shí)進(jìn)入休眠,等到生產(chǎn)者往緩沖區(qū)添加數(shù)據(jù)之后,再喚醒消費(fèi)者。通常采用進(jìn)程間通信的方法解決該問(wèn)題,常用的方法有信號(hào)燈法等。如果解決方法不夠完善,則容易出現(xiàn)死鎖的情況。出現(xiàn)死鎖時(shí),兩個(gè)線(xiàn)程都會(huì)陷入休眠,等待對(duì)方喚醒自己。
(上述介紹摘自百度百科)

  • NSCondition簡(jiǎn)單使用案例
    這里我們簡(jiǎn)單實(shí)現(xiàn)了下生產(chǎn)者生產(chǎn),然后消費(fèi)者消費(fèi)的一個(gè)簡(jiǎn)單案例,只有生產(chǎn)者通知消費(fèi)者可以消費(fèi)的這么一個(gè)簡(jiǎn)單的實(shí)現(xiàn)。
 (void)testConditon{
    
    _testCondition = [[NSCondition alloc] init];
    //創(chuàng)建生產(chǎn)-消費(fèi)者
    for (int i = 0; i < 50; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self producer];
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self consumer];
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self consumer];
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self producer];
        });
    }
}

- (void)producer{
    [_testCondition lock]; // 操作的多線(xiàn)程影響
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生產(chǎn)一個(gè) 現(xiàn)有 count %zd",self.ticketCount);
    [_testCondition signal]; // 信號(hào)
    [_testCondition unlock];
}

- (void)consumer{
 
     [_testCondition lock];  // 操作的多線(xiàn)程影響
    if (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        [_testCondition wait];
    }
    //注意消費(fèi)行為,要在等待條件判斷之后
    self.ticketCount -= 1;
    NSLog(@"消費(fèi)一個(gè) 還剩 count %zd ",self.ticketCount);
     [_testCondition unlock];
}
  • 消費(fèi)者來(lái)到方法consumer,發(fā)現(xiàn)如果共享區(qū)域的資源self.ticketCount如果是0了,那么要等待,[_testCondition wait];。如果不為0,那么進(jìn)行"消費(fèi)",資源數(shù)-1。
  • 然后生產(chǎn)者調(diào)用方法producer,對(duì)共享資源self.ticketCount進(jìn)行+1的操作,然后發(fā)送signal信號(hào),通知正在等待消費(fèi)的消費(fèi)者,生產(chǎn)好了一個(gè),可以喚醒一個(gè)等待消費(fèi)的"消費(fèi)者"。
  • 而上述操作公共資源self.ticketCount的時(shí)候都需要加鎖,防止多線(xiàn)程訪(fǎng)問(wèn)。
3.2 NSCondition源碼窺探
NSCondition(1)

NSCondition(2)

它也是對(duì)pthread的封裝。

  • init方法調(diào)用了pthread_mutex_initpthread_cond_init。
  • lock調(diào)用了pthread_mutex_lock,unlock調(diào)用pthread_mutex_unlock。
  • wait調(diào)用pthread_cond_wait.
  • signal調(diào)用的是pthread_cond_signal
  • broadcast調(diào)用的是pthread_cond_broadcast

4. NSConditionLock

NSConditionLock也是條件鎖,實(shí)際上是對(duì)NSCondition的再封裝。使用更加靈活。

  • 相關(guān)API
      //無(wú)條件鎖 和NSLock沒(méi)什么區(qū)別 底層都是pthread_mutex_lock 和pthread_mutex_unlock
        [conditionLock lock];
        [conditionLock unlock];
  
      //有條件鎖 當(dāng)condition滿(mǎn)足條件時(shí)加鎖,不滿(mǎn)足條件不執(zhí)行
      - (void)lockWhenCondition:(NSInteger)condition;

    //有條件,解鎖時(shí) condition設(shè)置成相應(yīng)的條件,符合這個(gè)條件的其他等待的線(xiàn)程就能執(zhí)行了。
      - (void)unlockWithCondition:(NSInteger)condition;
4.1 NSConditionLock使用示例
NSConditionLock示例

1、NSConditionLock的對(duì)象初始化條件為2,三個(gè)異步的線(xiàn)程,只有condition為2的條件里能執(zhí)行。
2、線(xiàn)程1執(zhí)行條件是1,所以不符合條件,不往下執(zhí)行。線(xiàn)程2滿(mǎn)足條件,可以執(zhí)行;線(xiàn)程3沒(méi)有條件限制,也可以執(zhí)行。
3、因?yàn)槎际钱惒蕉以诓l(fā)隊(duì)列中,線(xiàn)程2優(yōu)先級(jí)是DISPATCH_QUEUE_PRIORITY_LOW,線(xiàn)程3是defualt默認(rèn)優(yōu)先級(jí),而且線(xiàn)程2還睡眠了0.1,所以3大概率在2的前邊執(zhí)行。
4、而線(xiàn)程1要等著線(xiàn)程2執(zhí)行完把condition置為1才能執(zhí)行。

所以執(zhí)行的順序是3,2,1。

4.2 NSConditionLock源碼窺探分析

回到源碼,直接搜索NSConditionLock,實(shí)現(xiàn)如下。

open class NSConditionLock : NSObject, NSLocking {
    //1、_cond是NSCondition()的對(duì)象,_value是Int類(lèi)型。
    //方法open var condition返回的就是_value。所以這個(gè)_value實(shí)際上就是初始化condition的值
    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
    }

    //2、lock真實(shí)調(diào)用的是 open func lock(before limit: Date) -> Bool,傳入的參數(shù)是固定的 .distantFuture
    open func lock() {
        let _ = lock(before: Date.distantFuture)
    }

    //3、解鎖時(shí) 調(diào)用的是broadcast也就是pthread_cond_broadcast,和unlock。
    open func unlock() {
        _cond.lock()
#if os(Windows)
        _thread = INVALID_HANDLE_VALUE
#else
        _thread = nil
#endif
        _cond.broadcast()
        _cond.unlock()
    }
    
    //返回的是value
    open var condition: Int {
        return _value
    }

    //4、lock(whenCondition condition: Int) 底層實(shí)際調(diào)用的是lock(whenCondition condition: Int, before limit: Date) -> Bool
    //limit傳入的是固定的.distantFuture
    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)
    }

    
    //5、unlock(withCondition condition: Int)調(diào)用的是broadcast和unlock
    open func unlock(withCondition condition: Int) {
        _cond.lock()
#if os(Windows)
        _thread = INVALID_HANDLE_VALUE
#else
        _thread = nil
#endif
        _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
            }
        }
#if os(Windows)
        _thread = GetCurrentThread()
#else
        _thread = pthread_self()
#endif
        _cond.unlock()
        return true
    }
    
    open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
        _cond.lock()
        //線(xiàn)程不為空 或者 傳進(jìn)來(lái)的 _value和傳進(jìn)來(lái)的condition不相同進(jìn)入循環(huán)
        while _thread != nil || _value != condition {
            //解鎖條件 具體看_cond.wait的實(shí)現(xiàn),
            if !_cond.wait(until: limit) {
                _cond.unlock()
                return false
            }
        }
#if os(Windows)
        _thread = GetCurrentThread()
#else
        _thread = pthread_self()
#endif
        _cond.unlock()
        return true
    }
    
    open var name: String?
}

1、_cond是NSCondition()的對(duì)象,_value是Int類(lèi)型。所以這個(gè)_value實(shí)際上就是初始化condition的值。

2、lock真實(shí)調(diào)用的是 open func lock(before limit: Date) -> Bool,傳入的參數(shù)是固定的 .distantFuture。

3、解鎖時(shí) 調(diào)用的是broadcast也就是pthread_cond_broadcast,和unlock。

4、lock(whenCondition condition: Int) 底層實(shí)際調(diào)用的是lock(whenCondition condition: Int, before limit: Date) -> Bool,limit傳入的是固定的.distantFuture。

5、unlock(withCondition condition: Int)調(diào)用的是broadcast和unlock

  • wait(until limit: Date) -> Bool
 open func wait(until limit: Date) -> Bool {
#if os(Windows)
        return SleepConditionVariableSRW(cond, mutex, timeoutFrom(date: limit), 0)
#else
        //timeOut不成立 就走else 也就是timeout為nil(timeout為nil時(shí)是date傳的值不大于0) 就走else 否則跳過(guò)語(yǔ)句
        guard var timeout = timeSpecFrom(date: limit) else {
            return false
        }
        
        //接著等待
        return pthread_cond_timedwait(cond, mutex, &timeout) == 0
#endif
    }

  • timeSpecFrom
    public static let distantFuture = Date(timeIntervalSinceReferenceDate: 63113904000.0)

   public var timeIntervalSinceNow: TimeInterval {
        return self.timeIntervalSinceReferenceDate - CFAbsoluteTimeGetCurrent()
    }

private func timeSpecFrom(date: Date) -> timespec? {
    //date.timeIntervalSinceNow 不大于0 返回nil ,大于0 跳過(guò)這個(gè)語(yǔ)句。
    guard date.timeIntervalSinceNow > 0 else {
        return nil
    }
    let nsecPerSec: Int64 = 1_000_000_000
    let interval = date.timeIntervalSince1970
    let intervalNS = Int64(interval * Double(nsecPerSec))

    return timespec(tv_sec: Int(intervalNS / nsecPerSec),
                    tv_nsec: Int(intervalNS % nsecPerSec))
}

結(jié)合上述代碼,可以了解到wait方法,當(dāng)傳進(jìn)來(lái)的limit再次是distantFuture時(shí),timeSpecFrom才走guard語(yǔ)句中的返回,才不繼續(xù)等待。傳進(jìn)來(lái)的不是distantFuture返回的是當(dāng)前的時(shí)間,也就是大于0的值,是ture

distantFuture 從代碼定義看,是一個(gè)非常大的值。

(而這個(gè)終止等待的條件,有可能是在broadcast廣播的時(shí)候,或者在unlock觸發(fā)跳出while的條件,后面嘗試驗(yàn)證一下)。

5.讀寫(xiě)鎖的實(shí)現(xiàn)

  • 讀寫(xiě)鎖簡(jiǎn)介
    讀寫(xiě)鎖,又叫共享-獨(dú)占鎖。從命名上來(lái)看,讀寫(xiě)鎖擁有兩把鎖,讀鎖寫(xiě)鎖。它的特點(diǎn)是:
  • 同一時(shí)間只允許一個(gè)線(xiàn)程對(duì)共享資源進(jìn)行寫(xiě)操作;
  • 當(dāng)進(jìn)行寫(xiě)操作的時(shí)候,同一時(shí)間其他線(xiàn)程都會(huì)被阻塞;
  • 當(dāng)進(jìn)行讀操作的時(shí)候,同一時(shí)間所有的寫(xiě)操作都會(huì)被阻塞;
  • 當(dāng)進(jìn)行讀操作的時(shí)候,同一時(shí)間其他線(xiàn)程可以進(jìn)行讀操作共享資源;
具體實(shí)現(xiàn)

根據(jù)上述要求,我們可以總結(jié)出來(lái),就是寫(xiě)的時(shí)候,在寫(xiě)之前的所有操作要完畢,只能進(jìn)行一個(gè)寫(xiě)操作。這符合GCD柵欄函數(shù)的特性,柵欄里的內(nèi)容執(zhí)行之前,會(huì)阻塞后續(xù)的同一隊(duì)列上的任務(wù),柵欄之前的操作執(zhí)行完,才會(huì)執(zhí)行柵欄里的操作。所以寫(xiě)放到柵欄里,然后所有的任務(wù)并發(fā)就ok了。

- (void)testReaderAndWriter{
    self.testQueue = dispatch_queue_create("abc", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 50; i++) {
        [self writer];
        [self reader];
        [self reader];
        [self writer];
        [self reader];
    }
}

//讀者
- (void)reader{
    dispatch_async(self.testQueue, ^{
        NSLog(@"讀取剩余票數(shù)%lu",(unsigned long)self.ticketCount);
    });
}

//寫(xiě)操作
- (void)writer{
    //進(jìn)行寫(xiě)操作
    dispatch_barrier_sync(self.testQueue, ^{
        self.ticketCount++;
    });
}

實(shí)現(xiàn)結(jié)果

實(shí)現(xiàn)結(jié)果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 回顧之前 前文講到多線(xiàn)程原理,線(xiàn)程安全、線(xiàn)程阻塞、線(xiàn)程使用等;這節(jié)我們來(lái)分析一下有關(guān)線(xiàn)程安全的一部分:鎖,線(xiàn)程鎖。...
    孜孜不倦_閑閱讀 1,007評(píng)論 0 2
  • 轉(zhuǎn)發(fā):LockForiOS 又到了春天挪坑的季節(jié),想起多次被問(wèn)及到鎖的概念,決定好好總結(jié)一番。 翻看目前關(guān)于 iO...
    Cooci_和諧學(xué)習(xí)_不急不躁閱讀 2,357評(píng)論 1 22
  • 引言 鎖是開(kāi)發(fā)中最常用的同步工具,通過(guò)鎖來(lái)實(shí)現(xiàn)對(duì)臨界資源的訪(fǎng)問(wèn)控制,從而使目標(biāo)代碼段同一時(shí)間只會(huì)被一個(gè)線(xiàn)程執(zhí)行。這...
    卡布奇諾_95d2閱讀 1,091評(píng)論 0 2
  • 收錄:原文地址 翻看目前關(guān)于 iOS 開(kāi)發(fā)鎖的文章,大部分都起源于 ibireme 的 《不再安全的 OSSpin...
    iOS猿_員閱讀 2,779評(píng)論 1 16
  • 轉(zhuǎn)載:談 iOS 的鎖 又到了春天挪坑的季節(jié),想起多次被問(wèn)及到鎖的概念,決定好好總結(jié)一番。 翻看目前關(guān)于 iOS ...
    小鯤鵬閱讀 588評(píng)論 0 0

友情鏈接更多精彩內(nèi)容