iOS 锁的底层探索笔记

3,336 阅读15分钟

一、前言

了解锁的机制会有助于项目开发,从而避免项目中多个线程访问同一块资源引发数据混乱的问题,本文只是简单的介绍 iOS 开发中的几种锁应用场景,以及分析锁的底层实现原理,了解每个锁的原理之后,在项目中运用锁会更加得心应手。

二、互斥锁和自旋锁的概念

介绍锁之前,先来了解一下锁的基本概念,会有助于后面知识点的理解,如果已经掌握请跳过。

1. 互斥锁

互斥锁: (Mutual exclusion,缩写 Mutex)防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。 互斥锁又分为递归锁和非递归锁。

  • 递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用。
  • 非递归锁:不可重入,必须等锁释放后才能再次获取锁。

2. 自旋锁

自旋锁: 多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。

3. 互斥锁和自旋锁的区别

其实就是线程的区别,互斥锁在线程获取锁但没有获取到时,线程会进入休眠状态,等锁被释放时,线程会被唤醒,而自旋锁的线程则会一直处于等待状态,忙等待,不会进入休眠。

三、互斥锁

互斥锁在项目中用的最多,经常看见的应该就是 @synchronized,因为写法相当简单,但是 @synchronized 性能却是最差的,在 ibireme 的 不再安全的 OSSpinLock 一文中,有一张图片简单的比较了各种锁的加解锁性能:

了解完每个锁的性能之后,就开始本文的主题吧。

1. pthread_mutex

pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 APIpthread_mutex 表示互斥锁。互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。

2. @synchronized

@synchronized 是一种递归互斥锁。在项目中使用如下:

// 加锁 - 保证线程安全
@synchronized (self) {
    if (self.ticketCount > 0) {
        self.ticketCount--;
        NSLog(@"当前余票还剩:%ld张",self.ticketCount);
    }else{
        NSLog(@"当前车票已售罄");
    }
}

多线程最经典的案例就是卖票了,当多线程访问同一数据时,肯定会造成数据混乱,加上 @synchronized 锁之后就能正常访问了,这又是什么原因呢?那如果 obj 传入 nil 又会怎么样呢?想探究 @synchronized 底层的话呢,最简单的就是通过汇编分析一下底层调用的是什么东东,我们就可以在 @synchronized (self) 打断点,显示汇编,就能分析出 @synchronized 底层调用了 objc_sync_enterobjc_sync_exit ,一进一出就很有意思了,接下来就分析这两个函数。你可以在 这里 找到 objc-sync 的全部源码。

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
        // 如果 obj 传进来的是 nil,则什么都不做,没有加锁的效果
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}

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
        // 如果 obj 传进来的是 nil,则什么都不做,没有加锁的效果
    }
    return result;
}

首先会判断传进来的对象是否有值,如果没有则什么都不做,没有加锁效果,如果有值,则会通过调用 id2data 返回一个 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;

struct SyncList {
    SyncData *data;
    spinlock_t lock;
    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
// 使用多个并行列表来减少不相关对象之间的争用
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data

static StripedMap<SyncList> sDataLists;

SyncData 包含了一个指向另一个 SyncData 对象的指针,叫做 nextData,所以我们可以把每个 SyncData 结构体看做是链表中的一个元素。还包含一个 object(嗯就是我们给 @synchronized 传入的那个对象)和一个有关联的 recursive_mutex_t,它就是那个跟 object 关联在一起的锁。其中还有一个 threadCount,这个 SyncData 对象中的锁会被一些线程使用或等待,threadCount 就是此时这些线程的数量。它很有用处,因为 SyncData 结构体会被缓存,threadCount==0 就暗示了这个 SyncData 实例可以被复用。

SyncList 正如我在上面提过,你可以把 SyncData 当做是链表中的节点 每个 SyncList 结构体都有个指向 SyncData 节点链表头部的指针,也有一个用于防止多个线程对此列表做并发修改的锁。

上面最后一行,是一个全局 mapsDataLists 的声明,表里面的元素是 SyncList 类型,这是不是意味着 @synchronized 里面维护了一个全局哈希表,哈希表又存储了一个链表,有点意思。

对存储数据结构有一定了解之后,也对我们理解后面源码有一定的帮助,再然后我们就来看一下 id2data 都做了些啥。

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    
#if SUPPORT_DIRECT_THREAD_KEYS
    // 第一步
    // 检查每线程单项快速缓存中是否有匹配的对象
    bool fastCacheOccupied = NO;
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        fastCacheOccupied = YES;
        // 在快速缓存中找到匹配项
        if (data->object == object) {
            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: {
                // 如果是 entry,则对 lockCount 加 1,并通过 tls 保存
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:
                // 如果是 exit,则对 lockCount 减 1,并通过 tls 保存
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    // 如果 lockCount 为 0,则从高速缓存中删除
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // 解决原子因为可能会与并发获取发生冲突
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                break;
            }
            // 返回 data
            return result;
        }
    }
#endif
    // 第二步
    // 检查已拥有锁的每个线程高速缓存中是否有匹配的对象
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        // 如果有缓存,循环查找
        unsigned int i;
        for (i = 0; i < cache->used; i++) {
            SyncCacheItem *item = &cache->list[i];
            if (item->data->object != object) continue;
            
            // 找到了一个匹配对象
            result = item->data;
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
                
            switch(why) {
            case ACQUIRE:
                // 如果是 entry,则对 lockCount 加 1
                item->lockCount++;
                break;
            case RELEASE:
                // 如果是 exit,则对 lockCount 减 1
                item->lockCount--;
                if (item->lockCount == 0) {
                    // // 如果 lockCount 为 0,则从每个线程缓存中删除。
                    cache->list[i] = cache->list[--cache->used];
                    // 解决原子因为可能会与并发获取发生冲突
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                break;
            }
            return result;
        }
    }
    
    lockp->lock();
    {   //第三步
        // 如果线程和锁在的线程缓存都没找到,就去全局 map 里的链表寻找匹配的对象
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                // 发现哈希表中有 object
                result = p;
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            // 发现一个没有用过的 SyncData
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
        // 当前没有与对象关联的同步数据
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
        //  使用上面保存的 SyncData
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }
    // 第四步
    // 如果上面都没有找到,则分配一个新的同步数据并添加到列表中
    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;
    
 done:
    lockp->unlock();
    if (result) {
        // 只有创建的 SyncData 才能进入这里。
        // 所有的释放、检查和递归获取都是由上面的线程缓存处理
        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_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
#endif
        {
            // 保存在线程缓存中
            if (!cache) cache = fetch_cache(YES);
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }
    return result;
}

由上面源码分析得知,@synchronizeddata 增删改查,需要时间,所以这可能就是 @synchronized 性能是最慢的原因之一吧,但是使用起来确实简单,如果项目中对性能没有那么高的要求,可以使用,但是使用时需要注意传入进去的 obj 一定要有值,不然就没有效果了。

3. NSLock

NSLock 是对 mutex 普通锁的封装,使用也非常简单。

@implementation ThreadSafeQueue {
    NSMutableArray *_elements;
    NSLock *_lock;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        _elements = [NSMutableArray array];
        _lock = [[NSLock alloc] init];
    }
    return self;
}
- (void)push:(id)element {
    [_lock lock];
    [_elements addObject:element];
    [_lock unlock];
}
@end

知道使用后,那怎么探索原理呢?我们可以来到 NSLock 头文件,发现 NSLockFoundation 框架里的,OC 不开源,但是 swift 是开源的,接下来就可以来打 swift 的源码中寻找答案。swift源码链接

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() {
        pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
        pthread_cond_init(timeoutCond, nil)
        pthread_mutex_init(timeoutMutex, nil)
#endif
    }
    
    open func lock() {
        pthread_mutex_lock(mutex)
    }
    open func unlock() {
        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
    }

从上述源码中发现 NSLock 是一种互斥锁,也是非递归的互斥锁,所以在递归时,会造成线程的堵塞,比如下面这个例子。

    NSLock *testlock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            [testlock lock];
            if (value > 0) {
                NSLog(@"current value = %d",value);
                // 异步递归调用
                testMethod(value - 1);
            }
            [testlock unlock];
        };
        testMethod(10);
    });

testMethod 第一次 lock 时,还没有 unlock 就又调用 testMethod lock 一次,所以会造成线程的堵塞,无法往下执行,所以当发生这样的情况时,需要专门使用递归锁来解决这个问题,比如 NSRecursiveLock

4. NSRecursiveLock

NSRecursiveLock 怎么解决 NSLock 造成的堵塞的情况呢?很简单,只需要将 NSLock 换成 NSRecursiveLock 就可以了。

NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^testMethod)(int);
    testMethod = ^(int value){
        [recursiveLock lock];
        if (value > 0) {
            NSLog(@"current value = %d",value);
            testMethod(value - 1);
        }
        [recursiveLock unlock];
    };
    testMethod(10);
});

其实 NSRecursiveLock 是对 mutex 递归锁的封装,它的底层和 NSLock 差不多,只是初始化的时候将 attrs 设置为 PTHREAD_MUTEX_RECURSIVE 了。

open class NSRecursiveLock: NSObject, NSLocking {
    internal var mutex = _RecursiveMutexPointer.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() {
        super.init()
#if CYGWIN
        var attrib : pthread_mutexattr_t? = nil
#else
        var attrib = pthread_mutexattr_t()
#endif
        withUnsafeMutablePointer(to: &attrib) { attrs in
            pthread_mutexattr_init(attrs)
            // 设置 attrs 为 PTHREAD_MUTEX_RECURSIVE 类型
            pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
            // 如果是 NSLock,attrs 则为 nil
            pthread_mutex_init(mutex, attrs)
        }
#if os(macOS) || os(iOS)
        pthread_cond_init(timeoutCond, nil)
        pthread_mutex_init(timeoutMutex, nil)
#endif
    }

递归锁需要注意的一点就是很容易造成死锁,就拿刚刚的例子来说,如果循环100次执行当前代码,还使用递归锁就会造成死锁。

NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
for (int i = 0; i < 100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            [recursiveLock lock];
            if (value > 0) {
                NSLog(@"current value = %d",value);
                testMethod(value - 1);
            }
            [recursiveLock unlock];
        };
        testMethod(10);
    });
}

那如何解决这个问题呢?这时候就需要用到 @synchronized 了,因为 @synchronized 锁的是同一个对象,当下次再来的时候只会从 cache 里面取,而不会像 NSRecursiveLock 当线程 1 锁了一次之后,线程 2 来了还会再锁一次,所以造成了线程 1 等线程 2 解锁,线程 2 等线程 1 解锁的死锁状况。

  • 锁的使用总结:当只是普通线程安全的时候,使用 NSLock,而需要保证递归调用线程安全的时候,使用 NSRecursiveLock,而又需要循环,外界的线程也会造成影响的时候,特别注意死锁的问题,就算损失一点点性能,也要保证安全哦!

5. NSCondition

NSCondition 是一个条件锁, 条件对象同时充当给定线程中的锁和检查点,锁在测试条件并执行条件触发的任务时保护代码。检查点行为要求在线程执行其任务之前条件为 true,当条件不为真时,线程阻塞,它保持阻塞状态,直到另一个线程发出条件对象的信号。

NSCondition 底层是对 mutexcond 的封装,更加面向对象,我们使用起来也更加的方便简洁。

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

NSCondition 使用场景更适用于生产消费者模式,当你生产大于 0 的时候,我可以进行消费,如果一直等于 0 的时候,我就一直等你生产。例子如下:

// 线程1
// 删除数组中的元素
- (void)consumer
{
    [self.condition lock];
    if (self.dataArray.count == 0) {
        // 等待
        [self.condition wait];
    }
    // 注意消费行为,要在等待条件判断之后
    [self.dataArray removeLastObject];
    NSLog(@"删除了元素");
    [self.condition unlock];
}
// 线程2
// 往数组中添加元素
- (void)producer {
    [self.condition lock];
    sleep(1);
    [self.dataArray addObject:@"TestData"];
    NSLog(@"添加了元素");
    // 信号
    [self.condition signal];
    [self.condition unlock];
}

6. NSConditionLock

NSConditionLock 是对 NSCondition 的进一步封装,可以设置具体的条件值对象。NSConditionLock 可以确保只有在满足特定条件时,线程才能获得锁。一旦获得了锁并执行了代码的关键部分,线程就可以放弃锁并将相关条件设置为新的内容。接下来我们就来看一下源码吧。

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(whenCondition condition: Int, before limit: Date) -> Bool {
        // 使用 NSCondition 加锁
        _cond.lock()
        while _thread != nil || _value != condition {
            // 超时了
            if !_cond.wait(until: limit) {
                // 防止线层堵塞,解锁
                _cond.unlock()
                return false
            }
        }
        _thread = pthread_self()
        _cond.unlock()
        return true
    }
    open func unlock(withCondition condition: Int) {
        _cond.lock()
        _thread = nil
        _value = condition
        _cond.broadcast()
        _cond.unlock()
    }

NSConditionLock 的使用也很简单,也可以实现任务之间的依赖。

// 初始化 NSConditionLock,并设置 condition 的值为 2
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    // 需要等到 condition 为 1 的时候执行下面的代码
    [conditionLock lockWhenCondition:1];
    NSLog(@"线程 1");
    [conditionLock unlockWithCondition:0];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    // 因为 condition 为 2,所以执行下面的代码
    [conditionLock lockWhenCondition:2];
    NSLog(@"线程 2");
    // 解锁,并将 condition 设置为 1
    [conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 因为没有条件限制,所以可以直接执行下面的代码
    [conditionLock lock];
    NSLog(@"线程 3");
    [conditionLock unlock];
});

// 打印结果
线程 3
线程 2
线程 1

7. os_unfair_lock

os_unfair_lock 用于取代不安全的 OSSpinLock,从 iOS10 开始才支持 从底层调用看,等待 os_unfair_lock 锁的线程会处于休眠状态,并非忙等 需要导入头文件 #import <os/lock.h>

// 使用 API 的介绍
//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);

四、 自旋锁

1. OSSpinLock

上面已经介绍了 OSSpinLock 不再安全,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等 busy-wait 状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。

2. atomic 属性修饰符

一般我们在开发中,大部分属性的声明都会加上 nonatomic, 以提高数据的读取效率(即不使用同步锁),那么为什么属性即使声明为 atomic 依然不能保证线程安全呢?我来到 atomic 的源码中就能发现其中奥秘。

void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    // ...省略其他修饰符对属性的操作代码

    if (!atomic) {
        // 不是 atomic 修饰
        oldValue = *slot;
        *slot = newValue;
    } else {
        // 如果是 atomic 修饰,加一把同步锁,保证 setter 的安全
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
}

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    // ...省略无关紧要的代码
    
    // 非原子属性,直接返回值
    if (!atomic) return *slot;
    // 原子属性,加同步锁,保证 getter 的安全
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
}

通过上述源码可以得出:属性在调用 gettersetter 方法时,会加上同步锁, 即在属性在调用 gettersetter 方法时,保证同一时刻只能有一个线程调用属性的读/写方法。保证了读和写的过程是可靠的,但并不能保证数据一定是可靠的

举例:定义属性 NSInteger i 是原子的,对 i 进行 i = i + 1; 操作就是不安全的。 该表达式需要三步操作: 1.读取 i 的值存入寄存器; 2.将 i 加 1; 3.修改 i 的值; 如果在第1步完成的时候,i 被其他线程修改了,那么表达式执行的结果就会与预期的不一样,也就是不安全的。

3. 读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的 CPU 数。

读写锁具有以下特点:

  • 同一时间,只能有一个线程进行写的操作。
  • 同一时间,允许有多个线程进行读的操作。
  • 同一时间,不允许既有写的操作,又有读的操作。

读写锁的 API 使用

// 需要导入头文件
#include <pthread.h>

pthread_rwlock_t lock;
// 初始化锁
pthread_rwlock_init(&lock, NULL);
// 读-加锁
pthread_rwlock_rdlock(&lock);
// 读-尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写-加锁
pthread_rwlock_wrlock(&lock);
// 写-尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 销毁
pthread_rwlock_destroy(&lock);

五、 总结

这篇文章主要讲述了锁的一些简单原理,@synchronized 底层维护了一个哈希链表进行 data 的存储,使用 recursive_mutex_t 进行加锁,而 NSLockNSRecursiveLockNSConditionNSConditionLock 底层都是对 mutex 的封装,mutex 到底是什么东东,咱们有时间再介绍。OSSpinLock 由于不安全也没有对其进行过多的介绍,以及atomic 属性修饰符为什么不能保证数据安全。

最后感谢以下博客对我的启发:

深入理解 iOS 开发中的锁

不再安全的 OSSpinLock

关于 @synchronized,这儿比你想知道的还要多