iOS中的锁

作者: 01_Jack | 来源:发表于2017-01-16 21:52 被阅读1272次

    前言

    生活中的锁随处可见,锁的作用也不言而喻,本文小结一下iOS的锁。

    技能表

    • atomic (酱油君)
    • @synchronized
    • NSLock
    • NSConditionLock
    • NSRecursiveLock
    • NSCondition
    • dispatch_semaphore
    • OSSpinLock
    • os_unfair_lock
    • POSIX LOCK
    • NSDistributedLock (酱油君)
    atomic

    说到锁不得不提线程安全,说到线程安全,不得不提nonatomic与atomic的爱恨情仇。
    我们经常看到这样的描述:“nonatomic为非原子性非线程安全,atomic为原子性线程安全,但是atomic真的线程安全吗?”
    然后就没有然后了。。

    先来扒一下nonatomic和atomic会干什么

    nonatomic/atomic = getter + setter + ivar
    

    nonatomic生成的getter、setter没加锁,atomic生成的getter、setter有锁。所以当通过setter/getter而非ivar赋值/取值被atomic修饰的属性时,该属性是读写安全的。
    然而读写安全并不代表线程安全,那么什么是线程安全?

    线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据 【引自百科】

    • atomic非线程安全验证
    @interface ViewController ()
    
    @property (strong) NSString *info;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        //A
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                self.info = @"a";
                NSLog(@"A--info:%@", self.info);
            }
        });
        
        //B
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                self.info = @"b";
                NSLog(@"B--info:%@", self.info);
            }
        });    
    
    }
    
    @end
    

    根据线程安全定义,如果atomic为线程安全A输出应该永远为A--info:a,B输出应该永远为B--info:b

    来看控制台输出

    atomic

    OK,atomic非线程安全验证完毕,下面来说锁。

    @synchronized

    @synchronized是iOS中最常见的锁,用法很简单

        //A
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                @synchronized (self) {
                    _info = @"a";
                    NSLog(@"A--info:%@", _info);
                }
            }
        });
        
        //B
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                @synchronized (self) {
                    _info = @"b";
                    NSLog(@"B--info:%@", _info);
                }
            }
        });
    

    这样就可以确保A中输出均为A--info:a,B中输出均为B--info:b

    但是@synchronized()括号中只要写相同数据就可以吗?如果这个数据的地址在不断变化呢?比如这样:

       //A
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                @synchronized (_info) {
                    _info = @"a";
                    NSLog(@"A--info:%@", _info);
                }
            }
        });
        
        //B
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                @synchronized (_info) {
                    _info = @"b";
                    NSLog(@"B--info:%@", _info);
                }
            }
        });
    

    再来看控制台输出:

    @synchronized

    可见@synchronized()括号中只能写地址不变的数据。
    @synchronized会隐式添加异常处理,当发生异常时自动释放互斥锁,性能相对较低。

    NSLock

    NSLock是iOS中另一种较为常见的锁,进入NSLock.h中可以发现NSLock继承自NSObject并且遵守NSLocking协议。除此之外,在NSLock.h中还能看到NSConditionLock、NSRecursiveLock和NSCondition这3个类,他们也都是继承自NSObject并且遵守NSLocking协议。

    NSLocking协议定义了两个实例方法,lock和unlock对应着加锁与解锁

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

    NSLock、NSConditionLock、NSRecursiveLock、NSCondition对应的实例都可以通过lock/unlock来进行加锁/解锁。

    代码这样写就可以确保线程安全

        //A
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                [_lock lock];
                _info = @"a";
                NSLog(@"A--info:%@", _info);
                [_lock unlock];
            }
        });
        
        //B
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                [_lock lock];
                _info = @"b";
                NSLog(@"B--info:%@", _info);
                [_lock unlock];
            }
        });
    

    注意:lock与unlock操作必须在同一线程,否则结果不确定甚至会引起死锁

    除此之外,NSLock还提供另外两个方法,见名知意,不做过多解释。

    - (BOOL)tryLock; 
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    
    NSConditionLock

    NSConditionLock中有这么几个方法

    - (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
    
    @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;
    

    在初始化lock时给个condition,属性condition为readonly,此时也是在给这个属性赋值
    伪代码

    - (instancetype)initWithCondition:(NSInteger)condition {
            if (self =[ [NSConditionLock alloc] init]) {
                    [self setValue:@condition forKey:@"condition"];
            }
            return self;
    }
    

    利用condition加锁、解锁时伪代码是这样的

    - (void)lockWhenCondition:(NSInteger)condition {
            if (_condition == condition) [self lock];
    }
    
    - (void)unlockWithCondition:(NSInteger)condition {
          [self setValue:@condition forKey:@"condition"];
          [self unlock];
    }
    

    condition实现条件锁时(也可以不实现,直接调用协议方法lock),只有符合条件才能上锁,但是解锁为非条件,任意condition都可以解锁,此时设置的condition为下一次条件锁的condition。
    线程安全示例代码

       //A
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                [_lock lockWhenCondition:0];
                _info = @"a";
                NSLog(@"A--info:%@--condition:%zd", _info, _lock.condition);
                [_lock unlockWithCondition:1];
    
            }
        });
        
        //B
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                [_lock lockWhenCondition:1];
                _info = @"b";
                NSLog(@"B--info:%@--condition:%zd", _info, _lock.condition);
                [_lock unlockWithCondition:0];
            }
        });
    

    利用这个特性,我们可以设置依赖关系。通常- (void)lock- (void)unlockWithCondition:(NSInteger)condition 配合使用 ,- (void)lockWhenCondition:(NSInteger)condition- (void) unlock 配合使用,当然也可以混用。

    NSRecursiveLock

    NSRecursiveLock翻译成中文叫递归锁,顾名思义可处理同一方法内部多次上锁的场景

    static int i = 10;
    
    - (void)recursiveLock {
        [_lock lock];
        NSLog(@"NSRecursiveLock--%zd", i--);
        if (i >= 0) {
            [self recursiveLock];
        }
        [_lock unlock];
    }
    

    如果把这里的lock换成NSLock显然必死无疑(死锁)。不同于其他lock,虽然NSRecursiveLock可以多次上锁,但是只有当上的所有锁全被解锁后,其他线程才能再次获取到NSRecursiveLock

        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self recursiveLock];
        });
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                [_lock lock];
                NSLog(@"lock");
                [_lock unlock];
                NSLog(@"unlock");
            }
        });
    
    NSRecursiveLock
    NSCondition

    NSCondition中有这些方法

    - (void)wait; //挂起线程
    - (BOOL)waitUntilDate:(NSDate *)limit; //什么时候挂起线程
    - (void)signal; // 唤醒一条挂起线程
    - (void)broadcast; //唤醒所有挂起线程
    

    NSCondition可以手动控制线程的挂起与唤醒,很明显可以利用这个特性设置依赖

    基本用法:

        //A
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [_lock lock];
            NSLog(@"A线程加锁");
            [_lock wait];
            NSLog(@"A线程唤醒");
            [_lock unlock];
            NSLog(@"A线程解锁");
        });
        
        //B
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [_lock lock];
            NSLog(@"B线程加锁");
            [_lock wait];
            NSLog(@"B线程唤醒");
            [_lock unlock];
            NSLog(@"B线程解锁");
        });
        
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            sleep(2);
            [_lock signal];
        });
    
    NSCondition-1

    如果把[_lock signal]换成[_lock broadcast]

    NSCondition-2
    dispatch_semaphore

    dispatch_semaphore利用信号量进行锁定

    线程安全示例代码:

    - (void)semaphore {
        dispatch_semaphore_t dsema = dispatch_semaphore_create(1);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                dispatch_semaphore_wait(dsema, DISPATCH_TIME_FOREVER);
                _info = @"a";
                NSLog(@"A--info:%@", _info);
                dispatch_semaphore_signal(dsema);
            }
        });
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                dispatch_semaphore_wait(dsema, DISPATCH_TIME_FOREVER);
                _info = @"b";
                NSLog(@"B--info:%@", _info);
                dispatch_semaphore_signal(dsema);
            }
        });
    }
    
    
    /*! 
     * @param value
     *信号量的起始值,当传入的值小于零时返回NULL
     * @result
     * 成功返回一个新的信号量,失败返回NULL
     */
    dispatch_semaphore_t dispatch_semaphore_create(long value)
    
    /*!
     * @discussion
     * 信号量减1,如果结果小于0,那么等待队列中信号增量到来直到timeout
     * @param dsema
     * 信号量
     * @param timeout
     * 等待时间
     * 类型为dispatch_time_t,这里有两个宏DISPATCH_TIME_NOW、DISPATCH_TIME_FOREVER
     * @result
     * 若等待成功返回0,timeout返回非0
     */
    long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
    
    /*!
     * @discussion
     * 信号量加1,如果之前的信号量小于0,将唤醒一条等待线程
     * @param dsema 
     * 信号量
     * @result
     * 唤醒一条线程返回非0,否则返回0
     */
    long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
    

    OK,了解完3个函数都是干嘛用的,来试试水

    超时,线程唤醒

    - (void)semaphore {
        dispatch_semaphore_t dsema = dispatch_semaphore_create(0);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
                long a = dispatch_semaphore_wait(dsema, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
                NSLog(@"a--%ld", a);
        });
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            sleep(2);
            long b = dispatch_semaphore_signal(dsema);
            NSLog(@"b--%ld", b);
        });
    }
    
    semaphore-1

    线程未唤醒,未超时

    - (void)semaphore {
        dispatch_semaphore_t dsema = dispatch_semaphore_create(0);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
                long a = dispatch_semaphore_wait(dsema, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
                NSLog(@"a--%ld", a);
        });
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            sleep(1);
            long b = dispatch_semaphore_signal(dsema);
            NSLog(@"b--%ld", b);
        });
    }
    
    
    semaphore-2

    结果和想的一样,没啥可继续唠的。多一嘴,线程唤醒与否和是否超时没必然关系,要看代码怎么写。

    OSSpinLock

    OSSpinLock自旋锁,使用时需导入头文件#import <libkern/OSAtomic.h>

        // 初始化 unlock为0,lock为非0
        OSSpinLock spinLock = OS_SPINLOCK_INIT;
        // 加锁
        OSSpinLockLock(&spinLock);
        // 解锁
        OSSpinLockUnlock(&spinLock);
        // 尝试加锁
        BOOL b = OSSpinLockTry(&spinLock);
    
    - (void)OSSpinLock {
        OSSpinLock spinLock = OS_SPINLOCK_INIT;
        NSLog(@"加锁前:%zd", spinLock);
        OSSpinLockLock(&spinLock);
        NSLog(@"加锁后:%zd", spinLock);
        OSSpinLockUnlock(&spinLock);
        NSLog(@"解锁后:%zd", spinLock);
    }
    
    OSSpinLock-1

    再来看一张截图

    OSSpinLock-2

    OSSpinLock is deprecated in iOS 10.0 - Use os_unfair_lock() from <os/lock.h> instead

    由于自旋锁存在优先级反转问题(可查看YYKit作者的这篇文章 不再安全的 OSSpinLock),在iOS 10.0中被<os/lock.h>中的os_unfair_lock()取代

    os_unfair_lock

    os_unfair_lock iOS 10.0新推出的锁,用于解决OSSpinLock优先级反转问题

    
        // 初始化
        os_unfair_lock_t unfairLock = &(OS_UNFAIR_LOCK_INIT);
        // 加锁
        os_unfair_lock_lock(unfairLock);
        // 解锁
        os_unfair_lock_unlock(unfairLock);
        // 尝试加锁
        BOOL b = os_unfair_lock_trylock(unfairLock);
    
    POSIX LOCK

    POSIX LOCK为C语言级别的锁,需引入头像文件#import<pthread.h>
    线程安全示例代码:

    static pthread_mutex_t lock;
    - (void)pLock {
        
        pthread_mutex_init(&lock, NULL);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                pthread_mutex_lock(&lock);
                _info = @"a";
                NSLog(@"A--info:%@", _info);
                pthread_mutex_unlock(&lock);
            }
        });
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                pthread_mutex_lock(&lock);
                _info = @"b";
                NSLog(@"B--info:%@", _info);
                pthread_mutex_unlock(&lock);
            }
        });
    }
    

    POSIX LOCK不单有pthread_mutex_t还有pthread_cond_t等,因为不常用这里不做过多介绍。

    NSDistributedLock

    NSDistributedLock分布式锁,用于MAC OS开发,酱油路过

    死锁

    所谓死锁: 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

    虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件。
    1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
    2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
    3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
    4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。 【引自百科】

    相关文章

      网友评论

      • walreal:感觉self.info本身的setter,getter是线程安全的,输出"A-info:b", 是因为调用NSLog时,self.info正好被其他线程改掉了。 例子把atomic关键字线程安全的粒度扩大了。
        01_Jack:@walreal :+1:完整验证应该重写setter getter
      • 南栀倾寒:种类基本列全了 但是很多细节没有讲清楚
      • 水户洋平_Psist:在讲到NSCondition的时候 wait是挂起吧?为什么输出A线程唤醒呢 楼主指点。
        01_Jack:队列啊,FIFO 多开几个线程试一下就知道了
      • 未莱:你好帅😍😍

      本文标题:iOS中的锁

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