iOS中的锁

前言

生活中的锁随处可见,锁的作用也不言而喻,本文小结一下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占用的资源。 【引自百科】

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容

  • 写在前面 多线程在日常开发中能起到性能优化的作用,但是一旦没用好就会造成线程不安全,本文就来讲讲如何保证线程安全 ...
    M_慕宸阅读 529评论 0 5
  • iOS中的锁 前言 写在前面: 临界区:指的是一块对公共资源进行访问的代码,并非一种机制或是算法。 自旋锁:是用于...
    ROBIN2015阅读 893评论 0 7
  • 本文不介绍各种锁的高级用法,只是整理锁相关的知识点,帮助理解。 锁的作用 防止在多线程(多任务)的情况下对共享资源...
    HelloiWorld阅读 2,889评论 0 8
  • 锁的种类 互斥锁 自旋锁 互斥锁:保证在任何时候,都只有一个线程访问对象。当获取锁操作失败时,线程会进入睡眠,等待...
    ricefun阅读 377评论 0 1
  • 在平时的开发中经常使用到多线程,在使用多线程的过程中,难免会遇到资源竞争的问题,那我们怎么来避免出现这种问题那? ...
    IAMCJ阅读 3,092评论 2 25