iOS-锁的使用

344 阅读14分钟

在日常开发当中,在使用多线程开发的时候,为了保证多线程访问数据的安全,我们通常通过加锁来避免数据竞争,避免造成数据错乱的问题。 如下方代码所示:

@property (nonatomic ,assign) int count;
self.count = 50;
- (void)test {
    for (int i = 0; i < 30; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.count --;
            NSLog(@"%d",self.count);
        });
    }
}

我们的本意是count顺序减少30次,但是在多线程的环境下,运行结果如下: image.png 可以看到这里的运行结果与我们所预期的是不同的,这也就产生了线程安全问题。

原子属性-Atomic

在OC当中,我们定义属性的时候,可以设置原子属性,那么原子属性atomic能否解决该问题呢? image.png 可以看到原子属性atomic并没有解决线程安全问题,那么为什么原子属性无法解决线程安全问题呢?我们可以看下属性的setter/getter调用中如何处理atomicnonatomic

objc_setProperty

image.png image.png 在这里可以看到通过objc_setProperty函数设置属性的时候:

  • 如果是atomic,就会进行PropertyLocks[slot]加锁操作,设置完后unlock()进行解锁;
  • 如果是nonatomic,直接进行赋值,不会进行加锁;

objc_getProperty

image.pngobjc_setProperty函数中同样的:

  • 如果是atomic,返回值前先进行加锁;
  • 如果是nonatomic,直接返回值;

在这里也可以看出原子属性与非原子属性的区别:

  • 非原子属性其实就是赋值取值操作,因此在iOS开发中,出于性能考虑通常使用nonatomic
  • 原子属性则会进行加锁操作。

既然不论是set还是get,在atomic情况下都有进行加锁解锁操作,那么又为什么在上述示例代码中产生了线程安全问题呢?

其主要原因为原子属性加的这把锁范围太小了,原⼦属性只能保障set或者get的读写安全,但我们在使⽤属性的时候,往往既有set⼜有get,所以说原⼦属性并不是线程安全的。

锁的种类:

根据锁的实现不同,可以分为忙等待锁和闲等待锁。

  • 忙等待锁 忙等待锁就是当获取不到锁资时,线程就会一直 while 循环,不做任何事情,所以就被成为忙等待锁,也被称为自旋锁,由于没有线程调度,没有时间片的消耗,效率会更高。
  • 闲等待锁 闲等待锁就是当获取不到锁资源时,线程不用自旋,而是把当前线程放入到锁的等待队列,然后执行调度程度,把CPU让给其他线程执行,这也就是互斥锁

OC中锁的使用:

OC中的锁还是比较多的,比较常用的有NSLockNSCondition@synchronized等,这里主要说一下几把常用锁的使用。先看下实现NSLocking协议的几把锁。

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

NSLock

NSLock是一把互斥锁,并且是非递归锁,也就是不可以重复加锁和解锁(不适用于递归函数)。

API说明
- (void)lock;加锁
- (void)unlock解锁
- (BOOL)tryLock;尝试加锁。成功返回YES,失败返回NO。
- (BOOL)lockBeforeDate:(NSDate *)limit;在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO。
@property (nullable ,copy) NSString *name锁名称
使用还是比较简单的:
- (void)test {
    NSLock * cusLock = [[NSLock alloc] init];
    for (int i = 0; i < 30; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [cusLock lock];
            self.count --;
            [cusLock unlock];
            NSLog(@"%d",self.count);
         });
     }
}

NSCondition

NSCondition也是一把互斥锁,其一般使用在供不应求场景(消费>生产),API如下:

API说明
- (void)lock加锁
- (void)unlock解锁
- (void)wait阻塞当前线程,使线程进入休眠,等待唤醒信号。调用前必须已加锁。
- (void)waitUntilDate阻塞当前线程,使线程进入休眠,等待唤醒信号或者超时。调用前必须已加锁。
- (void)signal唤醒一个正在休眠的线程,如果要唤醒多个,需要调用多次。如果没有线程在等待,则什么也不做。调用前必须已加锁。
- (void)broadcast唤醒所有在等待的线程。如果没有线程在等待,则什么也不做。调用前必须已加锁。

案例:在一个生产消费的场景下,生产者生产产品,消费者消费产品,并且同一时间消费的数量大于生产的数量

- (void)nscondition_test {
    self.count = 20;
    for (int i = 0; i < 50; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self fm_production];
        });
    }
    for (int i = 0; i < 70; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self fm_consumption];
        });
    }
}
- (void)fm_production {
    [self.iCondition lock];
    self.count ++;
    NSLog(@"生产了一个产品,现有产品 : %d个",self.count);
    [self.iCondition signal];// 唤醒一个wait正在休眠的线程
    [self.iCondition unlock];
}

- (void)fm_consumption {
    [self.iCondition lock];
    while (self.count == 0) {
        [self.iCondition wait];
    }
    self.count --;
    NSLog(@"消费了一个产品,现有产品: %d个",self.count);
    [self.iCondition unlock];
}

image.png 可以看到除了一开始是生产大于消费,后边的几乎是每生产一个就消费一个。也就是说wait确实在等待资源的产生。 这里要注意的是signal函数会唤醒进入休眠的线程,但也有可能会产生虚假唤醒

虚假唤醒:当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功 解决虚假唤醒的方法也很简单一般用while(condition != true)循环判断是否条件为真

NSConditionLock

NSConditionLock也是一把互斥锁,并且还是一个自带条件的一把锁,也就是说可以传入condition来达到满足条件就执行的效果,也叫条件锁,下面是API。

API说明
- (void)lock加锁
- (void)unlock解锁
- (instancetype)initWithCondition:(NSinteger)初始化一个NSConditionLock对象
@property(readonly) NSInteger condition锁的条件
- (void)lockWhenCondition:(NSInteger)condition满足条件时加锁
- (BOOL)tryLock尝试加锁
- (BOOL)tryLockWhenCondition如果接受对象的condition与给定的condition相等,则尝试获取锁,不阻塞线程
- (void)unlockWithCondition:(NSInteger)condition解锁,重置锁的条件
- (BOOL)lockBeforDate:(NSDate *)limit在指定时间点之前获取锁
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit在指定的时间前获取锁

案例:

- (void)fm_testConditonLock{
    self.iConditionLock = [[NSConditionLock alloc] initWithCondition:3];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self.iConditionLock lockWhenCondition:3];
        NSLog(@"线程 1");
        [self.iConditionLock unlockWithCondition:2];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self.iConditionLock lockWhenCondition:2];
        NSLog(@"线程 2");
        [self.iConditionLock unlockWithCondition:1];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self.iConditionLock lockWhenCondition:1];
        NSLog(@"线程 3");
        [self.iConditionLock unlockWithCondition:0];
    });
}

image.png 条件锁一般用在顺序的执行任务当中,通过控制创建条件和解锁条件,按照执行顺序执行线程

如果要实现顺序的条件执行,在GCD中也可以通过使用信号量来实现,也就是dispatch_semaphore_t,上方由锁来控制的代码,也可以改成如下方式:

- (void)fm_dispatch_semaphore_t {
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务1");
        dispatch_semaphore_signal(sem);
    });
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务2");
        dispatch_semaphore_signal(sem);
    });
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务3");
    });
}

其运行结果如下:

image.png

NSRecursiveLock

NSRecursiveLock即是一把互斥锁,也是一把递归锁,也就是说这把锁可以重复调用。 使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程中会记录获取锁和释放锁的次数,只有最后两者平衡锁才被最终释放。

API说明
- (void)lock加锁
- (void)unlock解锁
- (BOOL)tryLock尝试加锁。成功返回YES,失败返回NO。
- (BOOL)lockBeforeDete:(NSDate *)limit在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO。

案例:

-(void)recursiveLock_test {
    [self.iRecursiveLock lock];
    self.count --;
    NSLog(@"%d",self.count);
    [self.iRecursiveLock unlock];
}

虽然这把锁是递归锁,但是在同一时刻只能被一条线程所拥有,且可被同一线程多次获取,而不会产生死锁。但是如果其被多线程操作的时候,也可能因为线程之间获取锁释放锁的互相等待而出现死锁情况。

pthread_mutex

以上的几把锁都是实现了NSLocking协议的几把锁,也就是都有lock和unlock方法,其实C的底层也提供了一把锁也就是pthread_mutex,下面就看下pthread_mutex的使用。

API说明
pthread_mutex_init(pthread_mutex_t mutex, const pthread_mutexattr_t attr)初始化锁,pthread_mutexattr_t可用来设置锁的类型。
pthread_mutex_lock(pthread_mutex_t mutex);加锁
pthread_mutex_trylock(*pthread_mutex_t *mutex);加锁,但是上面方法不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待,成功返回0.失败返回错误信息
pthread_mutex_unlock(pthread_mutex_t *mutex);解锁
pthread_mutex_destroy(pthread_mutex_t* mutex);使用完锁之后释放锁
pthread_mutexattr_setpshared();设置互斥锁的范围
pthread_mutexattr_getpshared()获取互斥锁的范围
案例:
- (void)fm_pthread_mutex {
    //非递归
    pthread_mutex_t lock0;
    pthread_mutex_init(&lock0, NULL);
    pthread_mutex_lock(&lock0);
    pthread_mutex_unlock(&lock0);
    pthread_mutex_destroy(&lock0);
    
    //递归
    pthread_mutex_t lock;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&lock, &attr);
    pthread_mutexattr_destroy(&attr);
    pthread_mutex_lock(&lock);
    pthread_mutex_unlock(&lock);
    pthread_mutex_destroy(&lock);
}

可以看到pthread_mutex这把锁的使用是要比实现了NSLocking协议的几把锁要繁琐些,需要手动进行初始化及销毁

os_unfair_lock

os_unfair_lock设计之初是为了替代OSSpinLock自旋锁的,但os_unfair_lock其实是一把互斥锁,在苹果官方文档的说明中,说明了这把锁不是通过忙等,而是通过线程的休眠来等待锁,所以其实也是一把互斥锁,下面就是API及案例:

API说明
os_unfair_lock_init初始化锁
os_unfair_lock_lock加锁
os_unfair_lock_unlock解锁
os_unfair_lock_trylock尝试加锁。如果成功返回true。如果锁已经被锁定则返回false
os_unfair_lock_assert_owner如果当前线程未持有指定的锁或者锁已经被解锁,则触发崩溃
os_unfair_lock_assert_not_owner如果当前线程持有指定的锁,则触发崩溃

案例:

-(void)unfairLock_test {
    os_unfair_lock_lock(&_unfairLock);
    self.count --;
    NSLog(@"%d",self.count);
    os_unfair_lock_unlock(&_unfairLock);
}

image.png

OSSpinLock

看了这么多互斥锁,那么有没有自旋锁,其实之前是有的,也就是OSSpinLock,但由于其本身的问题,也就是优先级翻转问题,被苹果移除,转由os_unfair_lock替代。

优先级翻转:当多个线程有优先级的时候,如果一个优先级低的线程先去访问某个数据,此时使用自旋锁进行了加锁,然后一个优先级高的线程又去访问这个数据,那么优先级高的线程因为优先级高会一直占着CPU资源,此时优先级低的线程无法与优先级高的线程争夺CPU 时间,从而导致任务迟迟完不成、锁无法释放。

API说明
- OS_SPINLOCK_INIT初始化锁
- OSSpinLockLock(&spinlock)加锁
- OSSpinLockUnlock(&spinlock)解锁
- OSSpinLockTry(&spinlock)尝试上锁,如果返回false,表示上锁失败,锁正在被其他线程持有。如果返回true,表示上锁成功。

读写锁的实现

日常开发中对于读写等大量占用资源的IO操作,通常使用异步线程,而为了保证数据的安全,有需要对数据进行加锁,这时也就需要去实现读写锁。要实现读写锁也就需要注意以下几点:

  • 多读单写:在同一时刻可以被多条线程进行读取数据的操作,但是在同一时刻只能有一条线程在写入数据。
  • 读写互斥:在同以时刻,读和写不能同时进行。
  • 写操作不能阻塞其他线程。 因此读操作可以通过开启异步线程+并发队列来实现,写操作通过栅栏函数dispatch_barriy_async就可以; 实现代码如下:
- (void)lg_read {
    // 异步读取
    dispatch_async(self.iQueue, ^{
        // 读取的代码
        NSString *ret = self.dataDic[@"name"];
        NSLog(@"%@",ret);
    });

}
- (void)lg_write: (NSString *)name {
    // 写操作
    dispatch_barrier_async(self.iQueue, ^{
        [self.dataDic setObject:name forKey:@"name"];
    });
}

运行效果如下: image.png

细说@synchronized这把锁

接下来说下这把功能强大@synchronized锁,说他强大,主要是因为不管几条线程,不管是否递归调用,它都能够支持,也就是说它是个递归互斥锁,多个线程可以重复获得这个锁并进入执行块里面的代码而不会导致死锁。

@synchronized的使用也比较简单

@synchronized (锁对象) {
     //需要执行的代码
}
//使用
NSObject *obj = [NSObject alloc];
@synchronized (obj) {

}

通过clang -rewrite-objc命令可以查看下@synchronized底层的具体实现:

{
    id _rethrow = 0; 
    id _sync_obj = (id)obj; 
    objc_sync_enter(_sync_obj);
    try {
            struct _SYNC_EXIT {
        _SYNC_EXIT(id arg) : sync_exit(arg) {}
            ~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
            id sync_exit;
            }

        _sync_exit(_sync_obj);
    }
}

可以看到@synchronized在编译之后,底层调用的其实是objc_sync_enterobjc_sync_exit这两个成对的函数。我们可以再源码中找到这两个函数的实现:

  • objc_sync_enter
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;
}
  • 如果传入的obj为空,则其实啥也没干
  • 如果不为空,调用了id2data函数,返回SyncData类型的data 首先来看下SyncData类型
//单向链表结构
typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData; //指向下一个元素
    DisguisedPtr<objc_object> object;//包装后的objc,DisguisedPtr一般用来包装对象
    int32_t threadCount;  // number of THREADS using this block。记录使用block的线程数量
    recursive_mutex_t mutex; //递归锁,其底层实现为os_unfair_lock
} SyncData;

整个SyncData记录了使用objc对象的多线程数量以及递归锁,这两个参数也是@synchronized能在多线程下递归调用的根本。那么id2data函数又是如何使用SyncData的呢?

整个id2data函数,非常长,但是大体上可以分成一下几个部分:

  • 从线程的共享缓存tls中快速查找SyncData
  • 从线程缓存中查找SyncData
  • 如果线程缓存中没有,就需要从全局的哈希表中查找SyncData
  • 如果也没有,就创建SyncData,缓存SyncData 因此,我们也是分成这几部分来分别分析:

从共享缓存tls获取

首先来看下什么是TLS

TLS(线程本地存储)是一种在多线程时使用的技术,它可以使你的全局变量、静态变量以及局部静态、静态成员变量成为线程独立的变量,即每个线程的TLS变量之间互不影响。例如:linux下的全局变量 errno,windows下的GetLastError ,线程A在设置了一个错误信息后,线程B又设置了一个错误信息,前一个线程设置的信息就被覆盖了。解决方法就是将这个全局变量设置为TLS变量,这样在用户看来errno是一个全局变量,实际上它是每个线程独立的。

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);//从全局hash表里,通过object,获取锁
    SyncData **listp = &LIST_FOR_OBJ(object);//从全局hash表里,通过object,获取指向SyncData单向链表对头指针
    SyncData* result = NULL;//定义查询结果

#if SUPPORT_DIRECT_THREAD_KEYS
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    //从TLS快速缓存中查找指向单向链表
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        //标记TLS快速缓存已经被占用
        fastCacheOccupied = YES;
        //匹配TLS快速缓存是否缓存了锁对象
        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            switch(why) {
            case ACQUIRE: {
                lockCount++; //记录锁的储量+1
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount); //更新TLS快速缓存的记录锁住的数量
                break;
            }
            case RELEASE:
                lockCount--; //记录锁的储量-1
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount); //更新TLS快速缓存的记录锁住的数量
                if (lockCount == 0) {
                    // remove from fast cache
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL); //从TLS快速缓存中移除
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);//记录使用b1ock线程个数+
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
    ...省略下方代码...
}
  • id2data函数首先就是通过tls_get_direct函数来获取TLS中存储的单向链表
  • 如果有结果返回,首先将fastCacheOccupied置为YES
  • 然后判断返回的单向链表是否是保存了锁对象的单向链表,如果是的话
    • 返回结果result就是该单向链表
    • TLS中获取记录锁的数量lockCount
    • 然后更新TLS快速缓存的记录锁住的数量
    • 其中ACQUIRE是加锁的时候传入的参数,RELEASE为解锁的时候传入的参数。
  • 如果找到的单向链表不是要找的单向链表,或者没有找到单向链表则进入到下一步:

从线程缓存中获取

//检查已拥有锁的每个线程缓存,以查找匹配的对象
    // Check per-thread cache of already-owned locks for matching object
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        unsigned int i;
        //遍历缓存 遍历线程缓存中每一个item
        for (i = 0; i < cache->used; i++) {
            SyncCacheItem *item = &cache->list[i];
            //从线程缓存中判断是否有存储@synchronize的參数object
            if (item->data->object != object) continue;
            //如果发现一个,则说明线程缓存里有缓存这个obiect
            // Found a match.
            result = item->data;
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
                
            switch(why) {
            case ACQUIRE: //加锁
                item->lockCount++; //记录锁的数量+1
                break;
            case RELEASE:  //解锁
                item->lockCount--; //记录锁的数量-1
                if (item->lockCount == 0) { //如果锁的数量减为0,则从线程缓存中移除
                    // remove from per-thread cache
                    cache->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }

这里首先要看一下SyncCache的结构

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

typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;
    SyncCacheItem list[0];
} SyncCache;

可以看到SyncCache中的list是一个SyncCacheItem类型的数组,而 SyncCacheItem则代表一个线程中的数据,包含SyncData结构体以及当前线程锁的数量。其大体结构如下图所示: image.png

因此第二部分的主要流程如下:

  • 首先通过fetch_cache函数来获取SyncCache结构体;
  • 然后遍历SyncCache结构中的list数组,获取SyncCacheItem
  • 通过SyncCacheItem中的data来判断是否是要找的SyncData单向链表
  • 如果是的话,就根据传入参数进行相应的加锁或解锁等操作
  • 如果在这一过程中也没有找到,就进入到第三步;

从哈希表中获取


    lockp->lock();
    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        //遍历全局Hash表 StripedMap
        for (p = *listp; p != NULL; p = p->nextData) {
            ////链表第一个元素*listp  nextData下一个元素
            if ( p->object == object ) {////匹配esynchronize的参数object
                result = p;
                // atomic because may collide with concurrent RELEASE
               
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        if ( (why == RELEASE) || (why == CHECK) )
            //当前没有与对象关联的SyncData //跳转到done
            goto done;
    
        // an unused one was found, use it
        if ( firstUnused != NULL ) { //找到一个没用过的,用它
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;  //跳转到done
        }
    }


 done:
    lockp->unlock();
    if (result) {
        // Only new ACQUIRE should get here.
        // All RELEASE and CHECK and recursive ACQUIRE are 
        // handled by the per-thread caches above.
        if (why == RELEASE) {
            // Probably some thread is incorrectly exiting 
            // while the object is held by another thread.
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) {
            //如果tls未被占用则把新添加的SyncData存储到TLS中
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
#endif
        {
            //如果tls被占用了,则把新添加的放到线程缓存中存储
            // Save in thread cache
            if (!cache) cache = fetch_cache(YES);
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

id2data函数的开头,我们获取了两个参数lockplistp image.png 在哈希表获取SyncData的过程中就用到了这两个参数,在这里,仍要看下这个哈希表的结构。 image.png LOCK_FOR_OBJLIST_FOR_OBJ宏函数其实就是为了快速获取sDataLists哈希表。

image.png

  • StripedMap类型的哈希表中,如果是真机下8张表,如果是模拟器下64张表。
  • indexForPointer函数是用来快速的获取目标对象在哪个表中,array数组为整体的哈希表, 这种类型的哈希表类似于一个桶结构,8个桶或者64个桶,简单来说通过地址取余的方式来获取具体在哪个表中,表内的结构为SyncList的结构体,里边为SyncData单链表,因此获取数据的时候需要nextData来遍历获取下一个。 返回到id2data函数中:
  • 对返回来的某个哈希表的头指针进行遍历,如果找到某个链表的值就是要找的目标对象,result赋值然后跳转至done
    • 如果是加锁操作ACQUIRE,且如果tls未被占用则把找到的SyncData存储到TLS中(也就是设置快速缓存),如果tls被占用了,则把找到的SyncData放到线程缓存中(也就是设置线程缓存
    • 如果是解锁操作,就返回nil
  • 如果没有找到目标对象,但是找到了未被使用的SyncData,就初始化该SyncData,然后也跳转到done
  • 如果哈希表中也没有,则就需要进行创建SyncData,创建缓存。

创建SyncData,创建缓存

 posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;
  • 首先通过posix_memalign申请内存空间,
  • 然后初始化及进行赋值
  • 最后把链表头节点指向新开辟的SyncData(因为之前哈希表的链表中都没有,所以哈希表的链表头结点也需要进行初始化)。

最后看下objc_sync_exit

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;
}

这部分的流程就比较简单了,如果找到解锁对象,调用tryUnlock进行解锁就可以了。

总结

最后用网上的一张图来总结下本文:

image.png