iOS 中的鎖(1)

iOS 中的鎖(1)

本文主要通過Objective-C語言進行體現(xiàn)忆植,其實跟Swift也差不多泣港。

本文從鎖的基本概念骑歹、NSLock售躁、@synchronized三個方面做了介紹坞淮。

1. 基本概念

鎖的存在主要就是解決資源搶奪的問題茴晋,在iOS中的鎖基本分為兩種,分別是互斥鎖自旋鎖回窘,其實讀寫鎖也可以算一種诺擅,但是讀寫鎖也是一種特殊的自旋鎖。另外對于條件鎖啡直、遞歸鎖烁涌、信號量基本都是上層的封裝實現(xiàn)。

1.1 互斥鎖

自旋鎖避免了進程上下文的調(diào)度開銷酒觅,因此對于線程只會阻塞很 短時間的場合是有效的烹玉。

  • 互斥鎖:顧名思義,就是相互排斥阐滩,是一種用于多線程編程中,防止兩條線程同時對同一公共資源(比如全局變量)進行讀寫的機制县忌。該目的通過將代碼切片成一個一個的臨界區(qū)而達成掂榔,也就是說一個線程獲得鎖后,其他線程在其釋放鎖之前都獲取不到鎖症杏。互斥鎖也分為兩種分別是遞歸鎖非遞歸鎖装获。
    • 遞歸鎖:對于互斥鎖我們可以使用遞歸的方式進行鎖定,簡答的說就是可以重新進行鎖定厉颤,在同一個線程釋放鎖前可以再次獲取鎖進行鎖定穴豫,并且不會造成死鎖。
    • 非遞歸鎖:跟遞歸鎖相反逼友,不可以重新被鎖定精肃,必須等鎖釋放后才能再次獲得鎖

1.2 自旋鎖

  • 自旋鎖:線程反復(fù)檢查鎖變量是否可用。由于線程在這一過程中保持執(zhí)行帜乞, 因此是一種忙等待司抱。一旦獲取了自旋鎖,線程會一直保持該鎖黎烈,直至顯式釋放习柠。簡單來說就是線程A獲取到鎖,在其釋放鎖之前照棋,線程B又來獲取鎖资溃,此時獲取不到,線程B就會不斷的進入循環(huán)烈炭,一直檢查鎖是否已被釋放溶锭,如果釋放,則能獲取到鎖梳庆。

1.3 互斥鎖和自旋鎖的區(qū)別

  • 互斥鎖:當線程獲取鎖卻沒有獲取到時線程會進入休眠狀態(tài)暖途,等鎖被釋放時卑惜,線程會被喚醒,同時獲取到鎖驻售,繼續(xù)執(zhí)行任務(wù)露久,互斥鎖會改變線程的狀態(tài)。
  • 自旋鎖:當線程獲取鎖但沒獲取到時欺栗,不會進入休眠毫痕,而是一直循環(huán)等待,線程始終處于活躍狀態(tài)迟几,不會改變線程的狀態(tài)消请。

1.4 使用場景

  • 互斥鎖:由于其會改變線程的狀態(tài),這就需要內(nèi)核不斷的調(diào)度線程資源类腮,因此效率上比自旋鎖要低一些臊泰。理論上來說其實不適合使用自旋鎖的地方都可以使用互斥鎖。
  • 自旋鎖:在等待鎖期間線程是活躍度蚜枢,所以這種活躍在一定時間內(nèi)是個死循環(huán)缸逃,會消耗更多的CPU資源,自旋鎖避免了進程上下文的調(diào)度開銷厂抽,因此對于線程只會阻塞很短時間的場合是有效的檀头。由于自旋鎖的線程活躍也就使得它在遞歸調(diào)用的時候會產(chǎn)生死鎖君丁。

1.4 死鎖

死鎖就是字面意思炕泳,鎖上了解不開棵癣,不解鎖就不能繼續(xù)執(zhí)行,基本就是兩個線程的相互等待藐守,最后誰也等不到挪丢,這里說明一下阻塞和死鎖的理解誤區(qū),阻塞就是不能繼續(xù)執(zhí)行了是線程內(nèi)的等待吗伤,死鎖是線程間的等待吃靠,本質(zhì)上是不一樣的。

1.5 其他鎖

  • 條件鎖:就是條件變量足淆,當進程的某些資源要求不滿足時就進入休眠巢块,也就是鎖住了。當資源被分配到了巧号,條件鎖打開族奢,進程繼續(xù)運行。
  • 信號量:其實信號量算不上鎖丹鸿,它只是一種更高級的同步機制越走,在互斥鎖中semaphore在僅取值0/1時的特例。信號量可以有更多的取值空間用來實現(xiàn)更加復(fù)雜的同步,而不單單是線程間互斥廊敌。

2. NSLock

NSLock在分類中屬于互斥鎖铜跑,是我們在使用Objective-C進行開發(fā)時常用的一種鎖÷獬海看了好多文章說NSLock非遞歸鎖锅纺,確實NSLock的遞歸上會引起阻塞或者崩潰,但是在同一線程內(nèi)NSLock也可以再次加鎖肋殴,所以在這一點也不絕對囤锉。

2.1 NSLock 定義

我們點擊跳轉(zhuǎn)到NSLock的定義處,源碼如下:

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}bv

2.2 NSLocking 協(xié)議

從上一節(jié)中我們可以看到NSLock遵守一個NSLocking的協(xié)議护锤,協(xié)議定義如下:

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

我們可以看到協(xié)議中的lockunlock方法就是我們常用的加鎖解鎖的方法官地。值得注意的是-lock-unlock必須在相同的線程中成對調(diào)用,否則就會產(chǎn)生未知的結(jié)果烙懦。

2.3 NSLock的其他方法

對于NSLock還有另外兩個方法和一個屬性驱入,定義在源碼的下面,代碼如下:


// 嘗試獲取鎖氯析,獲取到返回YES沧侥,獲取不到返回NO
- (BOOL)tryLock;

// 在指定時間前獲取鎖,能夠獲取到返回YES魄鸦,獲取不到返回NO
- (BOOL)lockBeforeDate:(NSDate *)limit;

// 鎖名稱,如果使用鎖出現(xiàn)異常癣朗,輸出的log中會有鎖的名稱打印
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

2.4 NSLock 使用示例

這里我們模擬一個售票系統(tǒng)拾因,如果不加鎖的話就會導(dǎo)致一張票被賣多次的情況;加鎖后才能保證票數(shù)的準確旷余。

2.4.1 基本用法示例


- (void)testNSLock3 {
    
    self.lock = [[NSLock alloc] init];
    NSThread *thread1 = [[NSThread alloc]initWithTarget:self selector:@selector(therad:) object:nil];
    thread1.name = @"1號窗口";
    [thread1 start];

    NSThread *therad2 = [[NSThread alloc]initWithTarget:self selector:@selector(therad:) object:nil];
    therad2.name = @"2號窗口" ;
//    therad2.threadPriority = 0.8;
    [therad2 start];

    NSThread *therad3 = [[NSThread alloc]initWithTarget:self selector:@selector(therad:) object:nil];
    therad3.name = @"3號窗口" ;
//    therad3.threadPriority = 1 ;
    [therad3 start];
}

//模擬售票
-(void)therad:(id)object{

    //票數(shù)100張
    static int number = 100 ;

    while (1) {
        // 線程加鎖绢记,提高數(shù)據(jù)訪問的安全性
        [self.lock lock];
        number--;
        NSLog(@"%@ %d",[[NSThread currentThread]name],number);
          //模擬等待
//        sleep(1);

        if (number == 0) { break ; }
        [self.lock unlock] ;
    }
}

2.4.2 tryLock

- (void)testNSLock5 {
    //主線程中
    NSLock *lock = [[NSLock alloc] init];
    
    //線程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        NSLog(@"線程1");
        sleep(10);
        NSLog(@"睡醒了");
        [lock unlock];
    });
    
    //線程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);//以保證讓線程2的代碼后執(zhí)行
        if ([lock tryLock]) {
            NSLog(@"線程2");
            [lock unlock];
        } else {
            NSLog(@"嘗試加鎖失敗");
        }
    });
}

打印結(jié)果:

打印結(jié)果.jpg

根據(jù)打印結(jié)果我們可以看到,tryLock返回NO后也不會阻塞線程正卧,還繼續(xù)執(zhí)行下面的代碼蠢熄。

2.4.3 lockBeforeDate

如果將2.4.2中的tryLock換成[lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]就會阻塞線程,它將在Date前嘗試加鎖炉旷,如果在指定時間前都不能加鎖則返回NO签孔,加鎖失敗后跟上面打印是一致的。如果加鎖成功打印結(jié)果如下:

打印結(jié)果:

打印結(jié)果.jpg

2.4.4 死鎖


- (void)viewDidLoad {
    [super viewDidLoad];
    self.lock = [[NSLock alloc] init];
//    [NSThread detachNewThreadSelector:@selector(testLock1) toTarget:self withObject:nil];
    [self testLock1];
}

- (void)testLock1 {
    [self.lock lock];
    NSLog(@"testLock1: lock");
    [self testLock2];
    [self.lock unlock];
    NSLog(@"testLock1: unlock");
}

- (void)testLock2 {
    [self.lock lock];
    NSLog(@"testLock2: lock");
    [self.lock unlock];
    NSLog(@"testLock2: unlock");
}

這里只會打印testLock1: lock窘行,在同一線程內(nèi)如果沒有解鎖就再次加鎖的話就會造成死鎖饥追。這里就是testLock2等待testLock1解鎖,而testLock1也在等testLock2解鎖罐盔。

2.5 NSLock 底層實現(xiàn)

通過上面的NSLock定義我們可以知道NSLock是在Foundation庫中實現(xiàn)的但绕,但是Foundation的開源代碼只在Swift中有,本著有就比沒有強的思想我們下載一個Swift CoreLibs Foundation源碼一探究竟。

open class NSLock: NSObject, NSLocking {
    internal var mutex = _MutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
    private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
    private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif

    public override init() {
#if os(Windows)
        InitializeSRWLock(mutex)
        InitializeConditionVariable(timeoutCond)
        InitializeSRWLock(timeoutMutex)
#else
        pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
        pthread_cond_init(timeoutCond, nil)
        pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
    }
    
    deinit {
#if os(Windows)
        // SRWLocks do not need to be explicitly destroyed
#else
        pthread_mutex_destroy(mutex)
#endif
        mutex.deinitialize(count: 1)
        mutex.deallocate()
#if os(macOS) || os(iOS) || os(Windows)
        deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
#endif
    }
    
    open func lock() {
#if os(Windows)
        AcquireSRWLockExclusive(mutex)
#else
        pthread_mutex_lock(mutex)
#endif
    }

    open func unlock() {
#if os(Windows)
        ReleaseSRWLockExclusive(mutex)
        AcquireSRWLockExclusive(timeoutMutex)
        WakeAllConditionVariable(timeoutCond)
        ReleaseSRWLockExclusive(timeoutMutex)
#else
        pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
        // Wakeup any threads waiting in lock(before:)
        pthread_mutex_lock(timeoutMutex)
        pthread_cond_broadcast(timeoutCond)
        pthread_mutex_unlock(timeoutMutex)
#endif
#endif
    }

    open func `try`() -> Bool {
#if os(Windows)
        return TryAcquireSRWLockExclusive(mutex) != 0
#else
        return pthread_mutex_trylock(mutex) == 0
#endif
    }
    
    open func lock(before limit: Date) -> Bool {
#if os(Windows)
        if TryAcquireSRWLockExclusive(mutex) != 0 {
          return true
        }
#else
        if pthread_mutex_trylock(mutex) == 0 {
            return true
        }
#endif

#if os(macOS) || os(iOS) || os(Windows)
        return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
#else
        guard var endTime = timeSpecFrom(date: limit) else {
            return false
        }
        return pthread_mutex_timedlock(mutex, &endTime) == 0
#endif
    }

    open var name: String?
}

根據(jù)源碼我們可以看到NSLock是對pthread互斥鎖(mutex)的封裝捏顺,我們可以看到在lockBbeforeLimit方法中會調(diào)用timedLock這個方法六孵,這也是在Date前實現(xiàn)加鎖的真正實現(xiàn),我們跳轉(zhuǎn)到該方法(PS:在源碼中有Windows平臺的實現(xiàn)幅骄,這里我們就不看了劫窒,直接看else的部分)進行查看:

timedLock 源碼:

private func timedLock(mutex: _MutexPointer, endTime: Date,
                       using timeoutCond: _ConditionVariablePointer,
                       with timeoutMutex: _MutexPointer) -> Bool {
    var timeSpec = timeSpecFrom(date: endTime)
    while var ts = timeSpec {
        let lockval = pthread_mutex_lock(timeoutMutex)
        precondition(lockval == 0)
        let waitval = pthread_cond_timedwait(timeoutCond, timeoutMutex, &ts)
        precondition(waitval == 0 || waitval == ETIMEDOUT)
        let unlockval = pthread_mutex_unlock(timeoutMutex)
        precondition(unlockval == 0)

        if waitval == ETIMEDOUT {
            return false
        }
        let tryval = pthread_mutex_trylock(mutex)
        precondition(tryval == 0 || tryval == EBUSY)
        if tryval == 0 { // The lock was obtained.
            return true
        }
        // pthread_cond_timedwait didn't timeout so wait some more.
        timeSpec = timeSpecFrom(date: endTime)
    }
    return false
}
  • 首先設(shè)定超時時間
  • 然后開啟while循環(huán)
  • 在循環(huán)內(nèi)通過pthread_cond_timedwait函數(shù)進行計時等待,線程進入休眠
  • 如果超時直接返回false
  • 如果等待沒超時昌执,并在這期間鎖被釋放烛亦,則線程被喚醒,再次通過pthread_mutex_trylock函數(shù)嘗試獲取鎖
  • 如果獲取成功則返回true
  • 如果沒有超時懂拾,但是別喚醒后也沒有獲取到鎖(被其他線程搶先獲得)煤禽,則重新計算超時時間進入下一次while循環(huán)

2.6 小結(jié)

至此我們對NSLock的分析就完畢了,總結(jié)如下:

  • 在使用NSLock的時候lockunlock方法的使用時成對出現(xiàn)的
  • 切記不要在同一線程中連續(xù)加鎖又不解鎖岖赋,防止死鎖的出現(xiàn)
  • tryLock方法不會阻塞線程
  • lockBeforeDate方法會在超時前阻塞線程

3. @synchronized

@synchronized是我們在使用Objective-C開發(fā)時使用最多的一把鎖了由于代碼簡單且方便實用深得廣大開發(fā)者喜歡檬果。但是很多人并不知道@synchronized底層實現(xiàn)是個遞歸鎖,不會產(chǎn)生死鎖唐断,且不需要程序猿手動去加鎖解鎖选脊。下面我們就慢慢揭開@synchronized的面紗。

3.1 @synchronized 實現(xiàn)探索

由于@synchronized是關(guān)鍵字脸甘,我們并不能直接查看它的具體實現(xiàn)恳啥。此時我們編寫如下代碼:并添加斷點,通過匯編進行初步探索

16045596538778.jpg

要想查看匯編則需要在Xcode->Debug->Debug Workflow->Always Show Disassembly選中丹诀。

匯編代碼.jpg

由上面的匯編代碼我們可以看到在NSLog的上下分別有objc_sync_enterobjc_sync_exit的調(diào)用(bl)钝的。其實這里objc_sync_enter就是加鎖,objc_sync_exit就是解鎖铆遭。一般objc開頭的方法硝桩,基本都是在objc源碼中,下面我們打開objc源碼一探究竟枚荣。此處使用的是objc4-779.1碗脊。

3.2 objc_sync_enter 探索

3.2.1 objc_sync_enter源碼分析

objc_sync_enter源碼:

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        ASSERT(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}

通過注釋我們可以看出該函數(shù):

  • 首先在objc上同步
  • 如果需要就分配一個與objc相關(guān)聯(lián)的遞歸鎖
  • 一旦獲得鎖就返回OBJC_SYNC_SUCCESS

其實代碼上也跟上面說的一致,只是還有些細微的處理:

  • 如果鎖定的對象是空的橄妆,就不進行任何加鎖操作
  • 這里的result一直是OBJC_SYNC_SUCCESS衙伶,所以說就算因為對象obj為空導(dǎo)致加鎖失敗也不會阻塞線程,而是直接向下執(zhí)行害碾。
  • 如果obj不為空痕支,那么回調(diào)用id2data函數(shù)獲取一個SyncData類型的對象
  • 這里如果獲取不到鎖會通過data->mutex.lock();阻塞當前線程,等其他線程釋放鎖后繼續(xù)向下執(zhí)行蛮原。

3.2.2 關(guān)于加鎖(id2data)進一步探索

id2data有兩個參數(shù)卧须,第一個是鎖定對象obj,第二個ACQUIRE這里是個枚舉值ACQUIRE的意思是加鎖,其實還有兩個分別是RELEASE意思是解鎖和CHECK檢查鎖的狀態(tài)花嘶。

我們在來看看SyncData是個什么東西笋籽?

SyncData源碼:

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

通過SyncData源碼我們可以看到SyncData是一個結(jié)構(gòu)體,擁有4個成員:

  • nextData:指向下一個節(jié)點椭员,像極了一個鏈表的節(jié)點
  • object:一個OC對象车海,其實它就是保存被鎖對象的obj的。
  • threadCount:記錄線程數(shù)隘击,其實就是調(diào)用synchronized代碼塊的線程數(shù)
  • mutex:遞歸鎖侍芝,這就底層實際的鎖,通過調(diào)用它的lock()方法實現(xiàn)加鎖操作埋同。

id2data函數(shù)分析

id2data.jpg

由于id2data代碼比較多州叠,這里我們通過折疊代碼先來簡單看看

  • 首先第一塊就是初始化一個局部的鎖,以及一個SyncData指針凶赁,用作在鏈表中查找用也就是鏈表的頭指針咧栗,還有一個result用作存儲返回結(jié)果
  • 第而部分就是在單線程的緩存中進行查找了(查找是為了查找加鎖對象)
  • 下一步分就是在單條線程上沒有查找到則需要進行全局緩存的查找
  • 接下來就是都沒有查找到,就說明是第一次給該OC對象加鎖虱肄,所以要進行第一次存儲
  • 最后就是對查找對象的處理了

下面我們來一步一步的分析致板,首先看看前三行代碼

第一行就是獲取一個鎖,這是個局部變量咏窿,在本函數(shù)內(nèi)需要使用的鎖斟或,看名字spinlock_t是個自旋鎖,那么我們來看看它的實現(xiàn)的集嵌,源碼如下:

using spinlock_t = mutex_tt<LOCKDEBUG>;

class mutex_tt : nocopy_t {
    os_unfair_lock mLock;
    // 此處省略80多行代碼...    
}

os_unfair_lock

/*!
 * @typedef os_unfair_lock
 *
 * @abstract
 * Low-level lock that allows waiters to block efficiently on contention.
 *
 * In general, higher level synchronization primitives such as those provided by
 * the pthread or dispatch subsystems should be preferred.
 *
 * The values stored in the lock should be considered opaque and implementation
 * defined, they contain thread ownership information that the system may use
 * to attempt to resolve priority inversions.
 *
 * This lock must be unlocked from the same thread that locked it, attempts to
 * unlock from a different thread will cause an assertion aborting the process.
 *
 * This lock must not be accessed from multiple processes or threads via shared
 * or multiply-mapped memory, the lock implementation relies on the address of
 * the lock value and owning process.
 *
 * Must be initialized with OS_UNFAIR_LOCK_INIT
 *
 * @discussion
 * Replacement for the deprecated OSSpinLock. Does not spin on contention but
 * waits in the kernel to be woken up by an unlock.
 *
 * As with OSSpinLock there is no attempt at fairness or lock ordering, e.g. an
 * unlocker can potentially immediately reacquire the lock before a woken up
 * waiter gets an opportunity to attempt to acquire the lock. This may be
 * advantageous for performance reasons, but also makes starvation of waiters a
 * possibility.
 */
OS_UNFAIR_LOCK_AVAILABILITY
typedef struct os_unfair_lock_s {
    uint32_t _os_unfair_lock_opaque;
} os_unfair_lock, *os_unfair_lock_t;

根據(jù)上面的注釋缕粹,也就是下面這句,os_unfair_lock是用來替代OSSpinLock這個自旋鎖的互斥鎖纸淮,不會自旋,在內(nèi)核中等待被喚醒亚享。所以說spinlock_t并不是如它的名字一般咽块,而是個互斥鎖。

Replacement for the deprecated OSSpinLock. Does not spin on contention but waits in the kernel to be woken up by an unlock.

第二行代碼獲取了一個SyncData類型的二重指針欺税,我們通過查看SyncData的定義知道它是一個鏈表結(jié)構(gòu)侈沪,所以說這個listp就是鏈表的頭指針。對于宏LIST_FOR_OBJ代碼如下:

#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};

可以看到這個宏是個全局靜態(tài)變量sDataLists晚凿,以obj為索引獲取值亭罪,獲取到的對象類型為StripedMap<SyncList>,同時對其取data取地址進行返回歼秽。對于StripedMap是一個類应役,如下圖:

Class StripedMap.jpg
  • 通過圖中的第834行代碼我們可以看到,p就是我們傳進來的對象obj,從數(shù)組中取值返回
  • 數(shù)組的索引通過indexForPointer這個方法計算得出箩祥。
  • *算法是取出對象地址院崇,然后對地址右移四位 異或 上地址右移9的結(jié)果,然后對StripeCount取余袍祖,這StripeCount在我們的iPhone上只有8位底瓣,在類的一開始就定義了。
  • 這個8就是iPhone真機上所能使用哈希表的大小
  • 對于哈希表就可能存在哈希沖突蕉陋,但是在這里是通過這個方法去取鏈表捐凭,這個哈希表的數(shù)據(jù)是某條鏈表的頭結(jié)點,并且每條鏈表中還會有很多個節(jié)點凳鬓,每個節(jié)點又保存了和不同對象相關(guān)聯(lián)的鎖茁肠。

第三行代碼就是定義了一個result值為NULL,是id2data()需要返回的結(jié)果村视,就沒啥說的了官套。


接下來是在單線程中查找,代碼如下:

單線程緩存查找.jpg
  • 首先我們看到的是一個宏定義SUPPORT_DIRECT_THREAD_KEYS它的值是1蚁孔,并且在#if defined(__PTK_FRAMEWORK_OBJC_KEY0)下才會被定義為1奶赔,雖然具體什么意思不太明了,但是大概能看出來是在objc的某個環(huán)境下使用杠氢,也正是我們需要研究的
  • 首先看注釋就能很好的理解該分支是在單線程中查找緩存
  • fastCacheOccupied標記當前線程的快速緩存是否已被占用
  • 通過SYNC_DATA_DIRECT_KEYtls_get_direct函數(shù)從線程的快速緩存中取出一個SyncData節(jié)點站刑,其實單線程私有數(shù)據(jù)只保存一個節(jié)點的地址。
  • 如果這個節(jié)點沒有值也就拜拜了鼻百,直接跳過绞旅,如果有值就標記fastCacheOccupied為YES,即使這個節(jié)點中沒有我們要找的被鎖對象
  • 如果快速緩存中恰好是當前對象關(guān)聯(lián)的鎖温艇,那么對這個鎖的計數(shù)+1因悲,如果是解鎖就-1(這些計數(shù)是當前線程的私有數(shù)據(jù),其他線程訪問不到)
  • 如果找到勺爱,則會返回晃琳,如果找不到就會到下一流程進行處理了

在單線程緩存中查找不到后,就會來到下面的全局緩存中進行查找

全局緩存查找.jpg
  • 首先通過fetch_cache函數(shù)獲取整體的緩存對象SyncCache

SyncCache 和 SyncCacheItem 結(jié)構(gòu)體:

兩個結(jié)構(gòu)體實現(xiàn)如下琐鲁,詳見注釋卫旱,對于SyncCacheItem就一看就明了了。

typedef struct SyncCache {
    unsigned int allocated; // 保存`SyncCacheItem`的總數(shù)
    unsigned int used;  // 保存使用的數(shù)量
    SyncCacheItem list[0]; // 緩存鏈表頭結(jié)點地址
} SyncCache;

typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block
} SyncCacheItem;

fetch_cache 源碼:

static SyncCache *fetch_cache(bool create)
{
    _objc_pthread_data *data;
    
    data = _objc_fetch_pthread_data(create);
    if (!data) return NULL;

    if (!data->syncCache) {
        if (!create) {
            return NULL;
        } else {
            int count = 4;
            data->syncCache = (SyncCache *)
                calloc(1, sizeof(SyncCache) + count*sizeof(SyncCacheItem));
            data->syncCache->allocated = count;
        }
    }

    // Make sure there's at least one open slot in the list.
    if (data->syncCache->allocated == data->syncCache->used) {
        data->syncCache->allocated *= 2;
        data->syncCache = (SyncCache *)
            realloc(data->syncCache, sizeof(SyncCache) 
                    + data->syncCache->allocated * sizeof(SyncCacheItem));
    }

    return data->syncCache;
}
  • 這里就是返回SyncCache數(shù)據(jù)围段,_objc_pthread_data也是一個結(jié)構(gòu)體
  • 其實在這個方法中就是通過_objc_fetch_pthread_data去獲取這個data顾翼,(函數(shù)內(nèi)還調(diào)用了好幾步)這里就不一層一層的去分析了,感興趣的可以自己點擊跳轉(zhuǎn)就跟一下
  • 這個方法其實重要的還是做了個擴容奈泪,初始值是4适贸,如果不夠了就進行2倍擴容

如果來到下面這段代碼就說明我們在任何緩存中都沒有找到當前對象的鎖灸芳,說明是第一次給這個對象加鎖。

16046316987295.jpg
  • 首先就是使用一開始定義的局部鎖變量進行加鎖
  • 然后通過局部變量p遍歷循環(huán)鏈表
  • 這里在遍歷過程中如果找到了要加鎖的對象取逾,但是能到這里說明這個對象不被任何線程占用耗绿,所以直接使用這個對象就可以了,不需要重新開辟內(nèi)存空間插入鏈表
  • 如果沒有的話就通過變量firstUnused記錄第一個沒有使用的節(jié)點
  • 如果不是加鎖到這里就可以goto done 了
  • 最后如果firstUnused不為空對result進行一系列賦值操作后就可以goto done 了
  • 過了這個代碼塊砾隅,說明是第一次加鎖使用緩存哈希表误阻,則創(chuàng)建個新的節(jié)點放在表頭(也有可能是某一條鏈表中節(jié)點都被使用,重新開啟一條鏈表晴埂,也就是上面提到的8條的哈希表中的一個)究反。

最后我們goto done

done.jpg

在done這個模塊主要是對result的一些處理

  • 首先是解鎖
  • 然后判斷result如果是空的話就直接調(diào)用下面的返回了,返回了一個NULL
  • 如果是解鎖的話就直接返回nil儒洛,如果也不是加鎖就報錯了精耐,如果加鎖對象不匹配也會報錯,其實這塊就是容錯處理琅锻,基本不會來到
  • 下面就是對快速緩存的處理卦停,如果線程的快速緩存沒有被占用就存儲到快速緩存中,如果被占用就將節(jié)點存儲到全局緩存中

關(guān)于快速緩存:前面分析的時候無論在線程緩存中是否找到被鎖的對象(前提是線程快速緩存存在)fastCacheOccupied都會被置為YES恼蓬,也就是說線程私有數(shù)據(jù)的快速緩存只緩存一次惊完,且只保存第一次的這一個節(jié)點指針。我覺得就是你鎖了一次处硬,下次在鎖的概率很大小槐,使用頻率也會超級高,因為在鎖定一個對象的時候大多情況都是多線程操作這個對象荷辕,在短時間內(nèi)操作頻率足夠高凿跳,如果不高的話可能也不至于用鎖,還有可能該對象的同步鎖已經(jīng)被其他線程緩存到其他線程的私有數(shù)據(jù)了疮方,當前線程又無法訪問其他線程的私有數(shù)據(jù)控嗜,如果替換的話,會重復(fù)緩存骡显。

3.2.3 objc_sync_exit

關(guān)于解鎖我們也是直接看源碼了疆栏,代碼如下:

// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        // @synchronized(nil) does nothing
    }
    

    return result;
}

解鎖的代碼就很簡單了,幾步判斷蟆盐,核心步驟也是通過id2data進行處理的,在加鎖代碼分析的時候多提到過遭殉。

  • 首先定義一個局部的result
  • 判斷obj是否有值石挂,沒值就是對空對象解鎖直接返回就好了
  • 如果有值就經(jīng)過id2data返回要解鎖的節(jié)點
  • 如果沒返回節(jié)點就給result賦值error然后返回
  • 如果返回值了就調(diào)用tryUnlock去解鎖
  • 如果解鎖失敗也是給result賦值error然后返回
  • 其他情況返回一開始定義的result的默認值就好了

3.3 @synchronized 總結(jié)

3.3.1 注意事項

  • @synchronized不能鎖定和解鎖空對象,所以在使用的時候一定要注意鎖定對象不能是空值否則就會出現(xiàn)加鎖失敗的情況险污,達不到我們預(yù)期的效果(空對象也不會影響代碼的執(zhí)行)
  • @synchronized對于指針不斷變化的屬性或者成員變量可能不符合我們想要通過鎖來解決問題的初衷痹愚,所以在使用它的時候要特別注意對加鎖對象生命手氣和指針指向的變化

3.3.2 @synchronized

  • @synchronized屬于遞歸鎖富岳,在同一線程內(nèi)可重新加鎖,在其內(nèi)部有個持有鎖的計數(shù)器
  • @synchronized加鎖是調(diào)用的int objc_sync_enter(id obj)函數(shù)
  • @synchronized解鎖是調(diào)用的int objc_sync_exit(id obj)函數(shù)
  • 在上面的兩個函數(shù)中都主要通過id2data這個函數(shù)來存儲和獲取SyncData鎖對象
    • id2data中首先在線程的快速緩存中查找鎖對象節(jié)點
    • 如果找不到就去全局緩存中查找
    • 如果全局緩存中沒有就說明是第一次鎖定該對象拯腮,會從全局緩存的鏈表中遍歷找到第一個空閑節(jié)點存儲該對象關(guān)聯(lián)的新節(jié)點
    • 如果沒找到空節(jié)點就重新開辟一個新的鏈表窖式,將該對象關(guān)聯(lián)的節(jié)點存儲為鏈表的表頭
    • 最后將節(jié)點的結(jié)果存儲到線程的私有數(shù)據(jù)中,并保存早全局緩存中
    • 對于解鎖跟以上步驟一致动壤,只是通過傳入的加解鎖標志why(枚舉類型)進行不同的處理萝喘,最后返回待解鎖節(jié)點,通過調(diào)用mutex.tryUnlock();進行解鎖
    • 在沒條線程中維護這鎖的計數(shù)琼懊,沒加鎖一次計數(shù)加一阁簸,解鎖則減一
    • 關(guān)于全局緩存,實際上是一張哈希表哼丈,通過對鎖對象obj的指針地址進行哈希計算得出索引
    • hash表的的Value是鏈表第一個節(jié)點(SyncData)的地址启妹,每個鏈表中每個節(jié)點對應(yīng)不同的鎖對象(SyncData
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市醉旦,隨后出現(xiàn)的幾起案子饶米,更是在濱河造成了極大的恐慌,老刑警劉巖车胡,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件檬输,死亡現(xiàn)場離奇詭異,居然都是意外死亡吨拍,警方通過查閱死者的電腦和手機褪猛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來羹饰,“玉大人伊滋,你說我怎么就攤上這事《又龋” “怎么了笑旺?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長馍资。 經(jīng)常有香客問我筒主,道長,這世上最難降的妖魔是什么鸟蟹? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任乌妙,我火速辦了婚禮,結(jié)果婚禮上建钥,老公的妹妹穿的比我還像新娘藤韵。我一直安慰自己,他們只是感情好熊经,可當我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布泽艘。 她就那樣靜靜地躺著欲险,像睡著了一般。 火紅的嫁衣襯著肌膚如雪匹涮。 梳的紋絲不亂的頭發(fā)上天试,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天,我揣著相機與錄音然低,去河邊找鬼喜每。 笑死,一個胖子當著我的面吹牛脚翘,可吹牛的內(nèi)容都是我干的灼卢。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼来农,長吁一口氣:“原來是場噩夢啊……” “哼鞋真!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起沃于,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤涩咖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后繁莹,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體檩互,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年咨演,在試婚紗的時候發(fā)現(xiàn)自己被綠了闸昨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡薄风,死狀恐怖饵较,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情遭赂,我是刑警寧澤循诉,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站撇他,受9級特大地震影響茄猫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜困肩,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一划纽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧锌畸,春花似錦勇劣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至卸耘,卻和暖如春退敦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蚣抗。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工侈百, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人翰铡。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓钝域,卻偏偏與公主長得像,于是被迫代替她去往敵國和親锭魔。 傳聞我的和親對象是個殘疾皇子例证,可洞房花燭夜當晚...
    茶點故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 寫在前面 多線程在日常開發(fā)中能起到性能優(yōu)化的作用,但是一旦沒用好就會造成線程不安全迷捧,本文就來講講如何保證線程安全 ...
    M_慕宸閱讀 532評論 0 5
  • 歡迎閱讀iOS探索系列(按序閱讀食用效果更加)iOS探索 alloc流程iOS探索 內(nèi)存對齊&malloc源碼iO...
    呂子喬_eabd閱讀 1,130評論 0 2
  • 1. 為什么多線程需要鎖织咧? 首先在多線程處理的時候我們經(jīng)常會需要保證同步,這是為啥呢漠秋,看一下下面這個例子: 這種時...
    木小易Ying閱讀 1,046評論 0 8
  • 這段時間的研究內(nèi)容的是鎖笙蒙,因為實際開發(fā)中用到的比較少,文中難免會有錯誤庆锦,希望能夠多多指正捅位。這篇博客的第一部分是一些...
    kikido閱讀 485評論 0 1
  • 什么會給多線程的安全造成隱患? 有了多線程技術(shù)支持搂抒,我們可以并發(fā)的進行多個任務(wù)艇搀,因此同一塊資源就有可能在多個線程中...
    RUNNING_NIUER閱讀 3,328評論 6 32