[iOS开发]iOS中相关锁


锁作为一种非强制的机制,被用来保证线程安全。每一个线程在访问数据或者资源前,要先获取(Acquire)锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用
注:不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了

iOS中锁的基本种类只有两种:互斥锁、自旋锁,其他的可能比如:条件锁、递归锁、信号量都是上层的封装和实现

自旋锁:

我们在weak的实现原理中有学习过自旋锁,对于每个SideTable中间都有自旋锁,同时也使用了分离锁给单个SideTable上锁。

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

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

自旋锁的种类

1. OSSpinLock

自从OSSpinLock出现了安全问题之后就废弃了。自旋锁之所以不安全,是因为自旋锁由于获取锁时,线程会一直处于忙则等待的死锁状态,造成了任务优先级的反转

OSSpinLock忙等的机制就可能造成高优先级一直running等待,占用CPU时间片,而低优先级任务无法抢占时间片,变成迟迟完不成,不释放锁的情况。

// 初始化
spinLock = OS_SPINKLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);

2. os_unfair_lock

weak实现部分的自旋锁就使用的是这个,自旋锁已经不安全了,苹果推出了os_unfair_lock,这个锁解决了优先级反转的问题

//创建一个锁
    os_unfair_lock_t unfairLock;
//初始化
    unfairLock = &(OS_UNFAIR_LOCK_INIT);
    //加锁
    os_unfair_lock_lock(unfairLock);
    //解锁
    os_unfair_lock_unlock(unfairLock);

互斥锁

进行互斥操作的锁,防止两条线程同时对同一公共资源(比如全局变量)进行读写操作。
在这里插入图片描述
互斥锁又分为:

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

对于递归锁我们要注意使用时死锁问题,前后代码相互等待就会死锁
对于非递归锁,我们强行使用递归就会造成堵塞而非死锁。

1. pthread_mutex

pthread_mutex就是互斥锁本身——当锁被占用,而其他线程申请锁时,不是使用忙等,而是阻塞线程并睡眠

// 导入头文件
#import <pthread.h>
// 全局声明互斥锁
pthread_mutex_t _lock;
// 初始化互斥锁
pthread_mutex_init(&_lock, NULL);
// 加锁
pthread_mutex_lock(&_lock);
// 这里做需要线程安全操作
// ...
// 解锁 
pthread_mutex_unlock(&_lock);
// 释放锁
pthread_mutex_destroy(&_lock);

2. @synchronized

@synchronized通过调用C++的recursive_mutex_lock进行上锁和解锁的步骤。

@synchronized需要一个参数,这个参数相当于信号量

//用在防止多线程访问属性上比较多
- (void)setTestInt:(NSInteger)testInt {
    @synchronized (self) {
        _testInt = testInt;
    }
}

@synchronized的底层实现

@synchronized的底层实现

  1. 从其注释中recursive mutex可以得出synchronized是递归锁
  2. 如果锁的对象obj不存在时分别会走objc_sync_nil()不做任何操作
BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
   );

这也就是@synchronized作为递归锁但能防止死锁的原因所在:在不断递归的过程中如果对象不存在了就会停止递归从而防止死锁
3. 正常情况下(obj存在)会通过id2data方法生成一个SyncData对象

  • nextData指的是链表中下一个SyncData
  • object指的是当前加锁的对象
  • threadCount表示使用该对象进行加锁的线程数
  • mutex即对象所关联的锁
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;
  1. 既然@synchronized能在任意地方(VC、View、Model等)使用,那么底层必然维护着一张全局的表(类似于weak表)。而从SyncList和SyncData的结构可以证实系统确实在底层维护着一张哈希表,里面存储着SyncList结构的数据。SyncList和SyncData的关系如下图所示:
    请添加图片描述
  2. 使用快速缓存。这里有个重要的知识点——TLSTLS全称为Thread Local Storage,在iOS中每个线程都拥有自己的TLS,负责保存本线程的一些变量, 且TLS无需锁保护。
    快速缓存的含义:定义两个变量SYNC_DATA_DIRECT_KEY/SYNC_COUNT_DIRECT_KEY,与tsl_get_direct/tls_set_direct配合可以从线程局部缓存中快速取得SyncCacheItem.dataSyncCacheItem.lockCount

如果在缓存中找到当前对象,就拿出当前被锁的次数lockCount,再根据传入参数类型(获取、释放、查看)对lockCount分别进行操作

  • 获取资源ACQUIRE:lockCount++并根据key值存入被锁次数
  • 释放资源RELEASE:lockCount–并根据key值减少被锁次数。如果次数变为0,此时锁也不复存在,需要从快速缓存移除并清空线程数threadCount
  • 查看资源check:不操作

lockCount表示被锁的次数,意味着能多次进入,从侧面表现出了递归性

  1. 慢速缓存查找。获取该线程下的SyncCache。这个逻辑分支是找不到确切的线程标记只能进行所有的缓存遍历
typedef struct {
    SyncData *data;             //该缓存条目对应的SyncData
    unsigned int lockCount;     //该对象在该线程中被加锁的次数
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;     //该缓存此时对应的缓存大小
    unsigned int used;          //该缓存此时对应的已使用缓存大小
    SyncCacheItem list[0];      //SyncCacheItem数组
} SyncCache;
  • SyncCacheItem用来记录某个SyncData在某个线程中被加锁的记录,一个SyncData可以被多个SyncCacheItem持有
  • SyncCache用来记录某个线程中所有SyncCacheItem,并且记录了缓存大小以及已使用缓存大小
  1. 全局哈希表查找。快速、慢速流程都没找到缓存就会来到这步——在系统保存的哈希表进行链式查找。
  • lockp->lock()并不是在底层对锁进行了封装,而是在查找过程前后进行了加锁操作
  • for循环遍历链表,如果有符合的就goto done。寻找链表中未使用的SyncData并作标记
  • 如果是RELEASE或CHECK直接goto done
  • 如果第二步中有发现第一次使用的的对象就将threadCount标记为1且goto done
  1. 生成新数据并写入缓存
    第三步情况均不满足(即链表不存在——对象对于全部线程来说是第一次加锁)就会创建SyncData并存在result里,方便下次进行存储

done分析:

  • 先将前面的lock锁解开
  • 如果是RELEASE类型直接返回nil
  • 对ACQUIRE类型和对象的断言判断
  • !fastCacheOccupied分支表示支持快速缓存且快速缓存被占用了,将该SyncCacheItem数据写入快速缓存中
  • 否则将该SyncCacheItem存入该线程对应的SyncCache中

其他问题:

  1. 不能使用非OC对象作为加锁条件——id2data中接收参数为id类型
  2. 多次锁同一个对象会有什么后果吗——会从高速缓存中拿到data,所以只会锁一次对象
  3. 都说@synchronized性能低——是因为在底层增删改查消耗了大量性能
  4. 加锁对象不能为nil,否则加锁无效,不能保证线程安全
- (void)test {
    _testArray = [NSMutableArray array];
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (self.testArray) {
                self.testArray = [NSMutableArray array];
            }
        });
    }
}

上面代码一运行就会崩溃,原因是因为在某一瞬间testArray释放了为nil,但哈希表中存的对象也变成了nil,导致synchronized无效化
解决方案:1. self同步锁 2. NSLock

3. NSLock

NSLock是非递归锁

NSLock是对互斥锁的简单封装

- (void)test {
    self.testArray = [NSMutableArray array];
    NSLock *lock = [[NSLock alloc] init];
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            self.testArray = [NSMutableArray array];
            [lock unlock];
        });
    }
}

NSLock在AFNetworking的AFURLSessionManager中有使用到

其就是对互斥锁的简单封装

如果对非递归锁强行使用递归调用,就会在调用时发生堵塞,并非死锁,第一次加锁之后还没出锁就进行递归调用,第二次加锁就堵塞了线程。(因为不会查询缓存)
在这里插入图片描述
从官方文档的解释里看的更清楚,在同一线程上调用NSLock的两次lock方法将永久锁定线程。同时官方文档重点提醒向NSLock对象发送解锁消息时,必须确保该消息是从发送初始锁定消息的同一线程发送的。

4. NSRecursiveLock

NSRecursiveLock是递归锁

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

如果我们在外层添加for循环

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

此时程序会crush
在这里插入图片描述

因为for循环在block内部对同一个对象进行了多次锁操作,直到这个资源身上挂着N把锁,最后大家都无法一次性解锁,也就是找不到解锁的出口。

即线程1中加锁1、同时线程2中加锁2-> 解锁1等待解锁2 -> 解锁2等待解锁1 -> 无法结束解锁——形成死锁

此时我们可以通过@synchronized对对象进行锁操作,会先从缓存查找是否有锁syncData存在。如果有,直接返回而不加锁,保证锁的唯一性。

在这里插入图片描述
同一线程可以多次获取而不会导致死锁的锁。

注意是同一线程

5. GCD信号量

在这里插入图片描述

6. NSCondition

  • NSCondition同样实现了NSLocking协议,所以它和NSLock一样,也有NSLocking协议的lock和unlock方法,可以当做NSLock来使用解决线程同步问题,用法完全一样
  • 同时,NSCondition提供更高级的用法。wait和signal,和条件信号量类似。比如我们要监听imageNames数组的个数,当imageNames的个数大于0的时候就执行清空操作。思路是这样的,当imageNames个数大于0时执行清空操作,否则,wait等待执行清空操作。当imageNames个数增加的时候发生signal信号,让等待的线程唤醒继续执行。
  • NSCondition和NSLock、@synchronized等是不同的是,NSCondition可以给每个线程分别加锁,加锁后不影响其他线程进入临界区。这是非常强大。
    但是正是因为这种分别加锁的方式,NSCondition使用wait并使用加锁后并不能真正的解决资源的竞争。比如我们有个需求:不能让m<0。假设当前m=0,线程A要判断到m>0为假,执行等待;线程B执行了m=1操作,并唤醒线程A执行m-1操作的同时线程C判断到m>0,因为他们在不同的线程锁里面,同样判断为真也执行了m-1,这个时候线程A和线程C都会执行m-1,但是m=1,结果就会造成m=-1.

7. NSConditionLock

  • lock不分条件,如果锁没被申请,直接执行代码,unlock不会清空条件,之后满足条件的锁还会执行
  • unlockWithCondition:我的理解就是设置解锁条件(同一时刻只有一个条件,如果已经设置条件,相当于修改条件)
  • lockWhenCondition:满足特定条件,执行相应代码
  • NSConditionLock同样实现了NSLocking协议,试验过程中发现性能很低。
  • NSConditionLock也可以像NSCondition一样做多线程之间的任务等待调用,而且是线程安全的。
- (void)executeNSConditionLock {
    NSConditionLock* lock = [[NSConditionLock alloc] init];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (NSUInteger i=0; i<3; i++) {
            sleep(2);
            if (i == 2) {
                [lock lock];
                [lock unlockWithCondition:i];
            }

        }
    });


    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self threadMethodOfNSCoditionLock:lock];
    });


    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self threadMethodOfNSCoditionLock:lock];
    });


}

-(void)threadMethodOfNSCoditionLock:(NSConditionLock*)lock{
    [lock lockWhenCondition:2];
    [lock unlock];

}

总结

请添加图片描述
可以看到除了 OSSpinLock 外,dispatch_semaphore 和 pthread_mutex 性能是最高的。有消息称,苹果在新的系统中已经优化了 pthread_mutex 的性能,所有它看上去和 dispatch_semaphore 差距并没有那么大了。

  • OSSpinLock不再安全,底层用os_unfair_lock替代
  • atomic只能保证setter、getter时线程安全,所以更多的使用nonatomic来修饰
  • 读写锁更多使用栅栏函数来实现
  • @synchronized在底层维护了一个哈希链表进行data的存储,使用recursive_mutex_t进行加锁
  • NSLockNSRecursiveLockNSConditionNSConditionLock底层都是对pthread_mutex的封装
  • NSCondition和NSConditionLock是条件锁,当满足某一个条件时才能进行操作,和信号量dispatch_semaphore类似
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值