美文网首页
iOS底层-- 锁

iOS底层-- 锁

作者: Engandend | 来源:发表于2020-08-26 17:02 被阅读0次

    手动目录

    • 什么是锁
    • 锁的工作机制
    • 锁的分类
    • 设计到锁的其他概念
    • 常用锁的用法
      @synchronized()
      NSLock
      信号量 dispatch_semaphore_t
      atomic
      条件锁(NSConditionLock 、NSCondition)
      读写锁 pthread_rwlock
    • 对比理解递归/非递归
    • 更多内容参考文章

    不再安全的 OSSpinLock一文中 提到以下问题
    1、列举出9种不同的锁(不完全)
    2、分析不同锁的执行效率- 测试代码
    3、提出OSSpinLock不一定安全:优先级反转------高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock

    锁的执行效率

    什么是锁

    锁是一种同步机制,用于多线程环境中对资源访问的限制,保护数据安全。
    锁只是用来保证数据的安全,不保证执行顺序

    锁的工作机制

    每一个线程在访问数据或者资源前,要先获取(Acquire) 锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待或循环访问,直到锁重新可用。

    锁的分类

    • 自旋锁:
      如果共享数据已经有其他线程加锁了,线程会以死循环的方式一直尝试去访问,一旦被访问的资源被解锁(忙等),则等待资源的线程会立即执行。
    • 互斥锁
      分为递归锁/非递归锁
      如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。

    自旋锁的效率高于互斥锁。但是比互斥锁耗性能

    自旋锁:atomic、OSSpinLock、dispatch_semaphore_t 、pthread_rwlock(读写锁)
    互斥锁:pthread_mutex、@ synchronized、NSLock、NSConditionLock 、NSCondition、NSRecursiveLock,os_unfair_lock

    os_unfair_lock 是苹果官方推荐的替换OSSpinLock的方案,但是它在iOS10.0以上的系统才可以调用。

    注意 @synthesize 和 @ synchronized 的写法

    设计到锁的其他概念

    • 临界区
      指的是一块对公共资源进行访问的代码,并非一种机制或是算法。

    • 读写锁:百度百科
      一种特殊的自旋锁,
      它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。
      能提高并发性,可以有多个线程来同时读,但是只允许一个线程来写。

    • 条件锁
      互斥锁的一种。
      有一个条件,当进程的某些资源要求不满足这个条件时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。

    • 递归锁
      互斥锁的一种
      同一个线程对同一把锁加锁多次,而不会引发阻塞。
      同一个线程必须保证,加锁的次数和解锁的次数相同,其他线程才能够顺利解锁

    • 非递归锁
      互斥锁的一种
      在不解锁的情况下,当同一个线程多次加锁时,会产生阻塞。

    常用锁的用法

    @synchronized()

    递归锁

    • 一般用法
      - (void)task5 {
        dispatch_queue_t t = dispatch_queue_create("je", DISPATCH_QUEUE_CONCURRENT);
        for (NSInteger i = 0 ; i < 1000; i ++) {
            dispatch_async(t, ^{
                @synchronized (self) {
                    NSLog(@"----%@",@(i));          // 打印顺序   不固定
                    [self.array addObject:@(1)];
                }
            });
        }
        NSLog(@"3");              // 打印顺序穿插在  中间
      }
      

    关于 @synchronized,这儿比你想知道的还要多这篇文章中讲了具体源码的实现。
    同时,你也可以自己通过源码去分析。,通过clang命令将 @synchronized (self) {} 编译成底层源码。可以找到入口函数

    {
                id _rethrow = 0;
                id _sync_obj = (id)appDelegateClassName;
                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);        // 释放锁
    
                } catch (id e) {_rethrow = e;}
                {
                    struct _FIN { _FIN(id reth) : rethrow(reth) {}
                        ~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
                        id rethrow;
                    }_fin_force_rethow(_rethrow);
                    
                }
            }
    

    在源码阅读中,有一些点需要注意:
    1、 @synchronized (obj) 中的obj 作为锁的标识符,只有标识符相同,才能满足互斥条件
    2、如果obj传入的是空(null),那么内部不做任务处理,也就是没有进行加锁解锁操作。

    因为是递归锁,我们可以写类似这样的代码:

      - (void)testLock{
      if(_count>0){ 
         @synchronized (obj) {
            _count = _count - 1;
            [self testLock];
          }
        }
    }
    

    而如果换成 NSLock ,它就会因为递归发生死锁了。

    NSLock

    非递归锁
    NSLock 属于 pthread_mutex 的一层封装, 设置了属性为nil pthread_mutex_init(mutex,nil) --- 在swift-corefoundation 开源代码里能看到。
    也有资料说nil 实际上是PTHREAD_MUTEX_ERRORCHECK

    它会损失一定性能换来错误提示。并简化直接使用 pthread_mutex 的定义。

    NSLock的API非常简单

    @protocol NSLocking
    - (void)lock;
    - (void)unlock;
    @end
    
    @interface NSLock : NSObject <NSLocking> {
    @private
        void *_priv;
    }
    - (BOOL)tryLock;
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    
    @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    @end
    

    lock:加锁
    unlock:解锁
    trylock:能加锁返回 YES 并执行加锁操作,相当于 lock,反之返回 NO,相当于判断当前锁的状态
    lockBeforeDate:这个方法表示会在传入的时间内尝试加锁,若能加锁则执行加锁操作并返回 YES,反之返回 NO
    name: 设置锁的名称

    NSLock的用法

    - (void)task5 {
        dispatch_queue_t t = dispatch_queue_create("je", DISPATCH_QUEUE_CONCURRENT);
        
        NSLock *lock = [NSLock new];
        
        for (NSInteger i = 0 ; i < 1000; i ++) {
            dispatch_async(t, ^{
                [lock lock];
                    NSLog(@"----%@",@(i));
                    [self.array addObject:@"1"];
                [lock unlock];
            });
        }
        NSLog(@"------------3");
    }
    

    信号量 dispatch_semaphore_t

    其用法在 上一篇文章 iOS底层--GCD应用 的信号量有讲解,不再赘述。

    atomic

    atomic 用于声明属性的时候的修饰符。与之对应的是 nonatomic。
    atomic相对于 nonatomic 来说,能够保证数据安全,但是执行效率低(因为内部要加锁,大概慢20倍)。一般不实用这个来修饰属性。

    关于 atomic 的知识点,可能涉及到2个面试题:
    1、atomic的原理
    2、atomic一定是安全的吗?

    • atomic的原理

    在 objc 源码中 通过 objc_setProperty_atomic 找到 真正的实现源码 reallySetProperty

    static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
    {
        if (offset == 0) {
            object_setClass(self, newValue);
            return;
        }
    
        id oldValue;
        id *slot = (id*) ((char*)self + offset);
    
        if (copy) {
            newValue = [newValue copyWithZone:nil];             // copy 修饰 进行copy操作
        } else if (mutableCopy) {
            newValue = [newValue mutableCopyWithZone:nil];      // mutableCopy 修饰 进行mutableCopy操作
        } else {
            if (*slot == newValue) return;
            newValue = objc_retain(newValue);                   // 其他 修饰 进行retain操作
        }
        
        if (!atomic) {                                          // 非原子修饰  直接新值替换掉旧值
            oldValue = *slot;
            *slot = newValue;
        } else {                                                // ⚠️ 原子修饰
            spinlock_t& slotlock = PropertyLocks[slot];         // mutex_tt --- os_unfair_lock  锁  
            slotlock.lock();                                    // 加锁
            oldValue = *slot;
            *slot = newValue;        
            slotlock.unlock();                                  // 解锁
        }
    
        objc_release(oldValue);
    }
    

    通过源码一目了然,内部通过 mutex_tt 加锁 在临界区内进行旧置换新值的操作。
    get方法同样可以找到

    • atomic 是否安全?
      我们常说的 atomic 能保证数据安全,是给予对数据的读写(set/get)来说的,对于数据的修改,其并不能保证数据安全
      比如说NSString *,我们可以在多线程中进行读写(set/get),是安全的。
      比如 NSMutableArray、NSMutableDictionary,进行读写(set/get)是安全的,但是如果对数据进行修改,就不再是安全的(增、删、改)。
    • 安全的写 --- 没有问题
    @property (atomic, strong) NSMutableArray *array;
    - (void)task5 {
        dispatch_queue_t t = dispatch_queue_create("je", DISPATCH_QUEUE_CONCURRENT);
        for (NSInteger i = 0 ; i < 1000; i ++) {
            dispatch_async(t, ^{
                _array = [NSMutableArray new];
            });
        }
    }
    
    // 这种写法  如果 @property (nonatomic, strong) NSMutableArray *array; 就是不安全的
    
    • 不安全的改 -- 数据不安全
    @property (atomic, strong) NSMutableArray *array;
    _array = [NSMutableArray new];
    
    - (void)task5 {
        dispatch_queue_t t = dispatch_queue_create("je", DISPATCH_QUEUE_CONCURRENT);
    //    NSLock *lock = [NSLock new];
        for (NSInteger i = 0 ; i < 1000; i ++) {
            dispatch_async(t, ^{
      //          [lock lock];
                [self.array addObject:@"1"];
       //         [lock unlock];
            });
        }
    }
    

    NSRecursiveLock - 递归锁

    NSRecursiveLock 也是对 pthread_mutex 的一层封装, 设置了属性为PTHREAD_MUTEX_RECURSIVE
    --- 在swift-corefoundation 开源代码里能看到。

    pthread_mutexattr_init(attrs)
    pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE)) 
    

    条件锁(NSConditionLock 、NSCondition)

    每次提到条件锁,我们一般用生产者和消费者老举例:
    在一个面包店里:面包师生产面包,将面包放入货架,消费者从货架上拿走面包,前提是这个货架上有面包才能拿走。在谈 iOS的锁一文中的条件变量中 也进行了类似的解释。

    在这篇文章中特别提到:(用循环测试 也会出现这个问题)
    实际操作 NSCondition 做 wait 操作时,如果用 if 判断,是不能保证消费者是线程安全的。 --- (文做中搜索 <生产者-消费者问题> 有具体解释)

    if(count==0){
        [condition wait];
    }
    

    所以为了保证消费者操作的正确,使用 while 循环中的判断,进行二次确认:

    while (count == 0) {
        [condition wait];
    }
    
    --- NSCondition

    非递归锁
    NSCondition 属于 pthread_mutex 的一层封装, 设置了属性为nil --- 在swift-corefoundation 开源代码里能看到。

    pthread_mutex_init(mutex, nil)
    pthread_cond_init(cond, nil)
    

    看到这里是不是觉得很熟悉? 和 NSLock 一样。不过是在此基础上 加上了一个条件标示。

    NSCondition 遵循NSLocking协议,具体API

    @interface NSCondition : NSObject <NSLocking> {
    - (void)wait;                              //进程进入等待状态
    - (BOOL)waitUntilDate:(NSDate *)limit;    //线程等待一定的时间
    - (void)signal;                            //唤醒一个等待的线程
    - (void)broadcast;                        //唤醒所有等待的线程
    
    @property (nullable, copy) NSString *name;
    

    先用代码建立生产/消费 面包的场景

    @property (nonatomic, assign) NSInteger number;    // 面包数量
    
    self.number = 0;      // 初始化面包数量为0
    
    //   开始上班工作
    - (void)task6 {
        for (NSInteger i = 0; i < 1000; i ++) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                [self takeAwayOne];          // 消费者来买面包
            });
            
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                [self productOne];           // 生产一个面包
            });
        }
    }
    
    - (void)takeAwayOne {        // 拿走一个面包
        self.number --;
        NSLog(@"拿走一个,剩余%ld",self.number);
    }
    
    - (void)productOne {        // 生产一个面包
        self.number ++ ;
        NSLog(@"------生产一个,剩余%@",@(self.number));
    }
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self task6];
    }
    

    多执行几次touchues ,发现有时候是先拿走,再生产,这样的结果显然不是我们理想的结果。

    为了解决这个问题,我们需要用锁来处理。
    生产/消费 都加锁,在因为消费的前提是有生产,所以在消费里面 wait、在生产里面发信号 signal

    @property (nonatomic, strong) NSCondition *condition;  //条件锁
    
    _condition = [[NSCondition alloc]init];      // 初始化锁
    
    
    - (void)takeAwayOne {
        [_condition lock];
        
        while (self.number == 0) {
            [_condition wait];
        }
        self.number --;
        NSLog(@"拿走一个,剩余%ld",self.number);
        [_condition unlock];
        
    }
    
    - (void)productOne {
        [_condition lock];
        self.number ++ ;
        NSLog(@"------生产一个,剩余%@",@(self.number));
        [_condition unlock];
        [_condition signal];
    }
    

    这样就能满足我们实际的场景。
    在生产者中, 是先 unlock 还是先signal? 我认为是都可以的。
    至于 broadcast 在什么情况下用? 从上面的例子看,如果生产者一次生产2个面包,但是消费者只买一个,那么我如果用signal ,就是生产2个,只让一个消费者来买走一个,剩下的一个 放在那里,其他想买的人都以为没有面包了,就一直等,是影响实际生产的。

    --- NSConditionLock

    NSConditionLock 其实是对NSCondition的封装 在此基础上添加一个 value(NSInteger)的一个条件
    -- 在swift-corefoundation 开源中能找到这样一段:

    
    open class NSConditionLock : NSObject, NSLocking {
        internal var _cond = NSCondition()          // 内用用的就是 NSCondition
        internal var _value: Int                    // 在NSCondition的基础上 加了 value的条件
    
        ....
            _cond.broadcast()          // 内部的发送信号 用的是广播,而不是 signal 。  一般来说 broadcast 比signal的效率低。
            _cond.unlock()
        .....
    }
    

    看其API

    @protocol NSLocking
    - (void)lock;                // 不管什么条件  直接锁
    - (void)unlock;              // 不管什么条件  直接解锁
    @end
    
    - (instancetype)initWithCondition:(NSInteger)condition;      // 初始化新分配的NSConditionLock对象并设置其条件
    @property (readonly) NSInteger condition;                            // 条件 (只读)
    - (void)lockWhenCondition:(NSInteger)condition;               // 根据条件加锁
    - (BOOL)tryLock;                                                    // 能否加锁
    - (BOOL)tryLockWhenCondition:(NSInteger)condition;    // 如果接收器的条件等于指定的条件,则尝试获取锁。
    - (void)unlockWithCondition:(NSInteger)condition;           // 放弃锁定并设置接收器的条件
    - (BOOL)lockBeforeDate:(NSDate *)limit;                            // 尝试在指定时间之前获取锁。
    - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;     // 尝试在指定时间之前获取锁。(condition 是匹配条件)
    
    @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    

    特别注意的是:
    unlockWithCondition:(NSInteger)condition 是放弃锁并重新设置条件。
    用为代码来理解流程

    [_conditionLock lockWhenCondition:0];
    类似于:
    if (value == 0) {
     [ _conditionLock lock];          // 符合条件 就进行加锁
    ...... 执行操作
    }
    
    [_conditionLock unlockWithCondition:0];
    类似于:
     [ _conditionLock unlock];      // 解锁
    value = 0;          // 重设条件
    [_conditionLock  broadcast];      // 发送广播 (告诉其他的线程任务,  服务value = 0 的可以执行了)
    }
    

    类似于银行取号办业务: 有20个人,每个人的号分别是 1---20;
    _conditionLock = [[NSConditionLock alloc]initWithCondition:1]; // 开始办业务了, 当前叫的号是1号

    [_conditionLock lockWhenCondition:5]; // 我持有5号,当前不是我的号 我要等着(直到有人通知 5号可以办业务 我才可以办)
    .......... 5号要办的业务
    [_conditionLock unlockWithCondition:7]; // 5号业务办完了, 我要通知7号办业务

    [_conditionLock lockWhenCondition:1]; // 我持有1号
    .......... 1号要办的业务
    [_conditionLock unlockWithCondition:2]; // 我的业务办完 了 通知2号来办业务。

    读写锁 pthread_rwlock

    读写锁的效果是:可以很对线程读,但是只能一个线程来写。
    那么原理就应该是这样的:异步读数据,栅栏写数据

    self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);       // 自定义一个并发队列
    
    - (id)readData:(id)key {
        __block id obj;
        dispatch_async(self.concurrent_queue, ^{
            /// 读取数据
        });
        return obj;
    }
    
    - (void)setDataObject:(id)obj forKey:(NSString *)key{
        dispatch_barrier_async(self.concurrent_queue, ^{
            // 设置数据
        });
    }
    
    

    对比理解递归/非递归

    递归 ----- 同一个线程对同一把锁加锁多次,而不会引发阻塞。
    非递归 -- 同一个线程多次加锁时,会产生阻塞。

    NSLock -----非递归
    NSRecursiveLock -----递归
    @synchronized() -----递归

    用这段代码来测试:

      - (void)task5 {
          NSLock *lock = [[NSLock alloc] init];
          
          dispatch_async(dispatch_get_global_queue(0, 0), ^{
              // 定义一个block 任务
              static void (^testMethod)(int);
              testMethod = ^(int value){
                  [lock lock];
                  if (value > 0) {
                      NSLog(@"current value = %d",value);
                      testMethod(value - 1);
                  }
                  [lock unlock];
              };
              
              testMethod(10);             // 在同一个线程进行多次加锁/解锁
          });
      }
    
    // 打印结果  
    11:33:00.347074+0800 [7720:95610] current value = 10
    只有这一条打印
    

    NSLock 是非递归锁,在同一个线程下多次加锁解锁,就会出现阻塞现象
    因为在 第一次执行testMethod(value - 1); 的时候 ,已经加了锁,再次执行这个代码的时候,还没有解锁,又进行了一次加锁,在没有解锁的情况下线程要进行休眠等待。 这样的话,这个线程就一直休眠,直到天荒地老。

    为了解决这个问题,可以用递归锁。

    方案一 :NSLock 换成 NSRecursiveLock
    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    
    方案二: 用 @synchronized
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
    static void (^testMethod)(int);
    testMethod = ^(int value){
        @synchronized (self) {
            if (value > 0) {
                NSLog(@"current value = %d",value);
                testMethod(value - 1);
            }
        }
    };
    
        testMethod(10);             // 在同一个线程进行多次加锁/解锁
    });
    

    NSRecursiveLock 与 @synchronized 都是递归锁,所以在同一个线程、未解锁的情况下,可以多次加锁而不会产生阻塞,可以正常执行。

    额外注意:

    上面的测试代码中,是在同一线程下进行多次加锁。
    如果在不同线程下,进行多次加锁会怎么样?

    - (void)task5 {
        NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
        
        for (NSInteger i = 0; i < 2000; i ++) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                
                // 定义一个block 任务
                static void (^testMethod)(int);
                testMethod = ^(int value){
                    if (value == 10) {
                        NSLog(@"%@",[NSThread currentThread]);
                    }
                    [lock lock];
                    if (value > 0) {
                        testMethod(value - 1);
                    }
                    [lock unlock];
                };
                
    //            调用任务
                testMethod(10);
            });
        }
    }
    

    这样就会产生死锁
    在不同的线程,同一个锁进行多次加锁,在某一个时刻,就可能产生 上一个锁还没解锁,下一个锁又来加锁。

    线程1: lock 等 线程2 的unlock
    线程2: lock 等 线程1的 unlock
    形成了相互等待,产生死锁。

    • 如何解决上面的问题?

      为了解决循环调用递归锁产生死锁,可以使用@ synchronized 来解决。
      (代码就不上了)

    为什么同样是递归锁,在不同线程循环调用的时候 NSRecursiveLock 不行,而@synchronized 可以?
    源码分析@synchronized 之后会发现 传入的参数(obj)的作用:
    当第一次进行synch加锁的时候,将信息存入syncdata里面,放入syncList 表里
    当第二次在进行加锁的时候,根据obj 来找,发现已经有锁了,就不进行lock 。直接用。

    总结:
    一般任务的锁:使用NSLock。
    递归调用的锁:使用NSRecursiveLock
    多线程 递归调用: 注意死锁的发生 。

    更多内容参考文章:

    谈 iOS 的锁🔥
    深入理解 iOS 开发中的锁
    iOS 开发中的八种锁(Lock)
    iOS开发中的11种锁以及性能对比 🔥

    相关文章

      网友评论

          本文标题:iOS底层-- 锁

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