美文网首页
iOS 中的锁(1)

iOS 中的锁(1)

作者: just东东 | 来源:发表于2020-11-10 10:20 被阅读0次

    iOS 中的锁(1)

    本文主要通过Objective-C语言进行体现,其实跟Swift也差不多。

    本文从锁的基本概念NSLock@synchronized三个方面做了介绍。

    1. 基本概念

    锁的存在主要就是解决资源抢夺的问题,在iOS中的锁基本分为两种,分别是互斥锁自旋锁,其实读写锁也可以算一种,但是读写锁也是一种特殊的自旋锁。另外对于条件锁递归锁信号量基本都是上层的封装实现。

    1.1 互斥锁

    自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很 短时间的场合是有效的。

    • 互斥锁:顾名思义,就是相互排斥,是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成,也就是说一个线程获得锁后,其他线程在其释放锁之前都获取不到锁。互斥锁也分为两种分别是递归锁非递归锁
      • 递归锁:对于互斥锁我们可以使用递归的方式进行锁定,简答的说就是可以重新进行锁定,在同一个线程释放锁前可以再次获取锁进行锁定,并且不会造成死锁。
      • 非递归锁:跟递归锁相反,不可以重新被锁定,必须等锁释放后才能再次获得锁

    1.2 自旋锁

    • 自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放。简单来说就是线程A获取到锁,在其释放锁之前,线程B又来获取锁,此时获取不到,线程B就会不断的进入循环,一直检查锁是否已被释放,如果释放,则能获取到锁。

    1.3 互斥锁和自旋锁的区别

    • 互斥锁:当线程获取锁却没有获取到时线程会进入休眠状态,等锁被释放时,线程会被唤醒,同时获取到锁,继续执行任务,互斥锁会改变线程的状态。
    • 自旋锁:当线程获取锁但没获取到时,不会进入休眠,而是一直循环等待,线程始终处于活跃状态,不会改变线程的状态。

    1.4 使用场景

    • 互斥锁:由于其会改变线程的状态,这就需要内核不断的调度线程资源,因此效率上比自旋锁要低一些。理论上来说其实不适合使用自旋锁的地方都可以使用互斥锁。
    • 自旋锁:在等待锁期间线程是活跃度,所以这种活跃在一定时间内是个死循环,会消耗更多的CPU资源,自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。由于自旋锁的线程活跃也就使得它在递归调用的时候会产生死锁。

    1.4 死锁

    死锁就是字面意思,锁上了解不开,不解锁就不能继续执行,基本就是两个线程的相互等待,最后谁也等不到,这里说明一下阻塞和死锁的理解误区,阻塞就是不能继续执行了是线程内的等待,死锁是线程间的等待,本质上是不一样的。

    1.5 其他锁

    • 条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。
    • 信号量:其实信号量算不上锁,它只是一种更高级的同步机制,在互斥锁中semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间用来实现更加复杂的同步,而不单单是线程间互斥。

    2. NSLock

    NSLock在分类中属于互斥锁,是我们在使用Objective-C进行开发时常用的一种锁。看了好多文章说NSLock非递归锁,确实NSLock的递归上会引起阻塞或者崩溃,但是在同一线程内NSLock也可以再次加锁,所以在这一点也不绝对。

    2.1 NSLock 定义

    我们点击跳转到NSLock的定义处,源码如下:

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

    2.2 NSLocking 协议

    从上一节中我们可以看到NSLock遵守一个NSLocking的协议,协议定义如下:

    @protocol NSLocking
    
    - (void)lock;
    - (void)unlock;
    
    @end
    

    我们可以看到协议中的lockunlock方法就是我们常用的加锁解锁的方法。值得注意的是-lock-unlock必须在相同的线程中成对调用,否则就会产生未知的结果。

    2.3 NSLock的其他方法

    对于NSLock还有另外两个方法和一个属性,定义在源码的下面,代码如下:

    
    // 尝试获取锁,获取到返回YES,获取不到返回NO
    - (BOOL)tryLock;
    
    // 在指定时间前获取锁,能够获取到返回YES,获取不到返回NO
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    
    // 锁名称,如果使用锁出现异常,输出的log中会有锁的名称打印
    @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    

    2.4 NSLock 使用示例

    这里我们模拟一个售票系统,如果不加锁的话就会导致一张票被卖多次的情况;加锁后才能保证票数的准确。

    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{
    
        //票数100张
        static int number = 100 ;
    
        while (1) {
            // 线程加锁,提高数据访问的安全性
            [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的代码后执行
            if ([lock tryLock]) {
                NSLog(@"线程2");
                [lock unlock];
            } else {
                NSLog(@"尝试加锁失败");
            }
        });
    }
    

    打印结果:

    打印结果.jpg

    根据打印结果我们可以看到,tryLock返回NO后也不会阻塞线程,还继续执行下面的代码。

    2.4.3 lockBeforeDate

    如果将2.4.2中的tryLock换成[lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]就会阻塞线程,它将在Date前尝试加锁,如果在指定时间前都不能加锁则返回NO,加锁失败后跟上面打印是一致的。如果加锁成功打印结果如下:

    打印结果:

    打印结果.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,在同一线程内如果没有解锁就再次加锁的话就会造成死锁。这里就是testLock2等待testLock1解锁,而testLock1也在等testLock2解锁。

    2.5 NSLock 底层实现

    通过上面的NSLock定义我们可以知道NSLock是在Foundation库中实现的,但是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?
    }
    

    根据源码我们可以看到NSLock是对pthread互斥锁(mutex)的封装,我们可以看到在lockBbeforeLimit方法中会调用timedLock这个方法,这也是在Date前实现加锁的真正实现,我们跳转到该方法(PS:在源码中有Windows平台的实现,这里我们就不看了,直接看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
    }
    
    • 首先设定超时时间
    • 然后开启while循环
    • 在循环内通过pthread_cond_timedwait函数进行计时等待,线程进入休眠
    • 如果超时直接返回false
    • 如果等待没超时,并在这期间锁被释放,则线程被唤醒,再次通过pthread_mutex_trylock函数尝试获取锁
    • 如果获取成功则返回true
    • 如果没有超时,但是别唤醒后也没有获取到锁(被其他线程抢先获得),则重新计算超时时间进入下一次while循环

    2.6 小结

    至此我们对NSLock的分析就完毕了,总结如下:

    • 在使用NSLock的时候lockunlock方法的使用时成对出现的
    • 切记不要在同一线程中连续加锁又不解锁,防止死锁的出现
    • tryLock方法不会阻塞线程
    • lockBeforeDate方法会在超时前阻塞线程

    3. @synchronized

    @synchronized是我们在使用Objective-C开发时使用最多的一把锁了由于代码简单且方便实用深得广大开发者喜欢。但是很多人并不知道@synchronized底层实现是个递归锁,不会产生死锁,且不需要程序猿手动去加锁解锁。下面我们就慢慢揭开@synchronized的面纱。

    3.1 @synchronized 实现探索

    由于@synchronized是关键字,我们并不能直接查看它的具体实现。此时我们编写如下代码:并添加断点,通过汇编进行初步探索

    16045596538778.jpg

    要想查看汇编则需要在Xcode->Debug->Debug Workflow->Always Show Disassembly选中。

    汇编代码.jpg

    由上面的汇编代码我们可以看到在NSLog的上下分别有objc_sync_enterobjc_sync_exit的调用(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;
    }
    

    通过注释我们可以看出该函数:

    • 首先在objc上同步
    • 如果需要就分配一个与objc相关联的递归锁
    • 一旦获得锁就返回OBJC_SYNC_SUCCESS

    其实代码上也跟上面说的一致,只是还有些细微的处理:

    • 如果锁定的对象是空的,就不进行任何加锁操作
    • 这里的result一直是OBJC_SYNC_SUCCESS,所以说就算因为对象obj为空导致加锁失败也不会阻塞线程,而是直接向下执行。
    • 如果obj不为空,那么回调用id2data函数获取一个SyncData类型的对象
    • 这里如果获取不到锁会通过data->mutex.lock();阻塞当前线程,等其他线程释放锁后继续向下执行。

    3.2.2 关于加锁(id2data)进一步探索

    id2data有两个参数,第一个是锁定对象obj,第二个ACQUIRE这里是个枚举值ACQUIRE的意思是加锁,其实还有两个分别是RELEASE意思是解锁和CHECK检查锁的状态。

    我们在来看看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是一个结构体,拥有4个成员:

    • nextData:指向下一个节点,像极了一个链表的节点
    • object:一个OC对象,其实它就是保存被锁对象的obj的。
    • threadCount:记录线程数,其实就是调用synchronized代码块的线程数
    • mutex:递归锁,这就底层实际的锁,通过调用它的lock()方法实现加锁操作。

    id2data函数分析

    id2data.jpg

    由于id2data代码比较多,这里我们通过折叠代码先来简单看看

    • 首先第一块就是初始化一个局部的锁,以及一个SyncData指针,用作在链表中查找用也就是链表的头指针,还有一个result用作存储返回结果
    • 第而部分就是在单线程的缓存中进行查找了(查找是为了查找加锁对象)
    • 下一步分就是在单条线程上没有查找到则需要进行全局缓存的查找
    • 接下来就是都没有查找到,就说明是第一次给该OC对象加锁,所以要进行第一次存储
    • 最后就是对查找对象的处理了

    下面我们来一步一步的分析,首先看看前三行代码

    第一行就是获取一个锁,这是个局部变量,在本函数内需要使用的锁,看名字spinlock_t是个自旋锁,那么我们来看看它的实现的,源码如下:

    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;
    

    根据上面的注释,也就是下面这句,os_unfair_lock是用来替代OSSpinLock这个自旋锁的互斥锁,不会自旋,在内核中等待被唤醒。所以说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的定义知道它是一个链表结构,所以说这个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) { }
    };
    

    可以看到这个宏是个全局静态变量sDataLists,以obj为索引获取值,获取到的对象类型为StripedMap<SyncList>,同时对其取data取地址进行返回。对于StripedMap是一个类,如下图:

    Class StripedMap.jpg
    • 通过图中的第834行代码我们可以看到,p就是我们传进来的对象obj,从数组中取值返回
    • 数组的索引通过indexForPointer这个方法计算得出。
    • *算法是取出对象地址,然后对地址右移四位 异或 上地址右移9的结果,然后对StripeCount取余,这StripeCount在我们的iPhone上只有8位,在类的一开始就定义了。
    • 这个8就是iPhone真机上所能使用哈希表的大小
    • 对于哈希表就可能存在哈希冲突,但是在这里是通过这个方法去取链表,这个哈希表的数据是某条链表的头结点,并且每条链表中还会有很多个节点,每个节点又保存了和不同对象相关联的锁。

    第三行代码就是定义了一个result值为NULL,是id2data()需要返回的结果,就没啥说的了。


    接下来是在单线程中查找,代码如下:

    单线程缓存查找.jpg
    • 首先我们看到的是一个宏定义SUPPORT_DIRECT_THREAD_KEYS它的值是1,并且在#if defined(__PTK_FRAMEWORK_OBJC_KEY0)下才会被定义为1,虽然具体什么意思不太明了,但是大概能看出来是在objc的某个环境下使用,也正是我们需要研究的
    • 首先看注释就能很好的理解该分支是在单线程中查找缓存
    • fastCacheOccupied标记当前线程的快速缓存是否已被占用
    • 通过SYNC_DATA_DIRECT_KEYtls_get_direct函数从线程的快速缓存中取出一个SyncData节点,其实单线程私有数据只保存一个节点的地址。
    • 如果这个节点没有值也就拜拜了,直接跳过,如果有值就标记fastCacheOccupied为YES,即使这个节点中没有我们要找的被锁对象
    • 如果快速缓存中恰好是当前对象关联的锁,那么对这个锁的计数+1,如果是解锁就-1(这些计数是当前线程的私有数据,其他线程访问不到)
    • 如果找到,则会返回,如果找不到就会到下一流程进行处理了

    在单线程缓存中查找不到后,就会来到下面的全局缓存中进行查找

    全局缓存查找.jpg
    • 首先通过fetch_cache函数获取整体的缓存对象SyncCache

    SyncCache 和 SyncCacheItem 结构体:

    两个结构体实现如下,详见注释,对于SyncCacheItem就一看就明了了。

    typedef struct SyncCache {
        unsigned int allocated; // 保存`SyncCacheItem`的总数
        unsigned int used;  // 保存使用的数量
        SyncCacheItem list[0]; // 缓存链表头结点地址
    } 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数据,_objc_pthread_data也是一个结构体
    • 其实在这个方法中就是通过_objc_fetch_pthread_data去获取这个data,(函数内还调用了好几步)这里就不一层一层的去分析了,感兴趣的可以自己点击跳转就跟一下
    • 这个方法其实重要的还是做了个扩容,初始值是4,如果不够了就进行2倍扩容

    如果来到下面这段代码就说明我们在任何缓存中都没有找到当前对象的锁,说明是第一次给这个对象加锁。

    16046316987295.jpg
    • 首先就是使用一开始定义的局部锁变量进行加锁
    • 然后通过局部变量p遍历循环链表
    • 这里在遍历过程中如果找到了要加锁的对象,但是能到这里说明这个对象不被任何线程占用,所以直接使用这个对象就可以了,不需要重新开辟内存空间插入链表
    • 如果没有的话就通过变量firstUnused记录第一个没有使用的节点
    • 如果不是加锁到这里就可以goto done 了
    • 最后如果firstUnused不为空对result进行一系列赋值操作后就可以goto done 了
    • 过了这个代码块,说明是第一次加锁使用缓存哈希表,则创建个新的节点放在表头(也有可能是某一条链表中节点都被使用,重新开启一条链表,也就是上面提到的8条的哈希表中的一个)。

    最后我们goto done

    done.jpg

    在done这个模块主要是对result的一些处理

    • 首先是解锁
    • 然后判断result如果是空的话就直接调用下面的返回了,返回了一个NULL
    • 如果是解锁的话就直接返回nil,如果也不是加锁就报错了,如果加锁对象不匹配也会报错,其实这块就是容错处理,基本不会来到
    • 下面就是对快速缓存的处理,如果线程的快速缓存没有被占用就存储到快速缓存中,如果被占用就将节点存储到全局缓存中

    关于快速缓存:前面分析的时候无论在线程缓存中是否找到被锁的对象(前提是线程快速缓存存在)fastCacheOccupied都会被置为YES,也就是说线程私有数据的快速缓存只缓存一次,且只保存第一次的这一个节点指针。我觉得就是你锁了一次,下次在锁的概率很大,使用频率也会超级高,因为在锁定一个对象的时候大多情况都是多线程操作这个对象,在短时间内操作频率足够高,如果不高的话可能也不至于用锁,还有可能该对象的同步锁已经被其他线程缓存到其他线程的私有数据了,当前线程又无法访问其他线程的私有数据,如果替换的话,会重复缓存。

    3.2.3 objc_sync_exit

    关于解锁我们也是直接看源码了,代码如下:

    // 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是否有值,没值就是对空对象解锁直接返回就好了
    • 如果有值就经过id2data返回要解锁的节点
    • 如果没返回节点就给result赋值error然后返回
    • 如果返回值了就调用tryUnlock去解锁
    • 如果解锁失败也是给result赋值error然后返回
    • 其他情况返回一开始定义的result的默认值就好了

    3.3 @synchronized 总结

    3.3.1 注意事项

    • @synchronized不能锁定和解锁空对象,所以在使用的时候一定要注意锁定对象不能是空值否则就会出现加锁失败的情况,达不到我们预期的效果(空对象也不会影响代码的执行)
    • @synchronized对于指针不断变化的属性或者成员变量可能不符合我们想要通过锁来解决问题的初衷,所以在使用它的时候要特别注意对加锁对象生命手气和指针指向的变化

    3.3.2 @synchronized

    • @synchronized属于递归锁,在同一线程内可重新加锁,在其内部有个持有锁的计数器
    • @synchronized加锁是调用的int objc_sync_enter(id obj)函数
    • @synchronized解锁是调用的int objc_sync_exit(id obj)函数
    • 在上面的两个函数中都主要通过id2data这个函数来存储和获取SyncData锁对象
      • id2data中首先在线程的快速缓存中查找锁对象节点
      • 如果找不到就去全局缓存中查找
      • 如果全局缓存中没有就说明是第一次锁定该对象,会从全局缓存的链表中遍历找到第一个空闲节点存储该对象关联的新节点
      • 如果没找到空节点就重新开辟一个新的链表,将该对象关联的节点存储为链表的表头
      • 最后将节点的结果存储到线程的私有数据中,并保存早全局缓存中
      • 对于解锁跟以上步骤一致,只是通过传入的加解锁标志why(枚举类型)进行不同的处理,最后返回待解锁节点,通过调用mutex.tryUnlock();进行解锁
      • 在没条线程中维护这锁的计数,没加锁一次计数加一,解锁则减一
      • 关于全局缓存,实际上是一张哈希表,通过对锁对象obj的指针地址进行哈希计算得出索引
      • hash表的的Value是链表第一个节点(SyncData)的地址,每个链表中每个节点对应不同的锁对象(SyncData

    相关文章

      网友评论

          本文标题:iOS 中的锁(1)

          本文链接:https://www.haomeiwen.com/subject/thidbktx.html