ios 锁的应用-读写锁
前言
上一篇博客底层分析-锁我们主要探索了@synchronized底层实现原理,知道了这把锁为什么可以多线程递归加锁。同时也浅尝辄止了每把锁都是不同的,如果使用不好会造成死锁,下面继续探索锁的种类以及实现一把读写锁。
锁的归类
其实基本的锁就包括了三类 自旋锁、互斥锁、读写锁,其他的比如条件锁,递归锁,信号量都是上层的封装和实现!
自旋锁:线程反复检查锁变量是否可用,由于线程在这一过程中保持执行,因此是一种
忙等状态
。一旦获取了自旋锁,线程会一直保持该锁,直至显示释放自旋锁。自旋锁避免了线程上下文的调度开销
,因此对于线程只会阻塞很短的场合是有效的。如OSSpinLock
、atomic
互斥锁:防止多条线程对同一公共资源(比如全局变量)进行读写机制。该目的是通过将代码切片成一个个临界区而达成。其实简单的说同一时刻保证有一条线程执行任务,其他线程会处在睡眠状态。如
NSLock
、pthread_mutex
、@synchronized
条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。如NSCondition、NSConditionLock
递归锁:就是同一个线程可以加锁N次而不会引发死锁。如NSRecursiveLock、pthread_mutex
信号量:semaphore是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥
读写锁:读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁。
自旋锁和互斥锁异同
共同点
都能保证同一时刻只能有一个线程操作锁住的代码
不同点
互斥锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入 睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。
自旋锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。
自旋锁应用场景:比较适合做一些不耗时的操作
锁的性能对比
通过开源框架LockPerformance对比锁的开锁和解锁的性能如下 OSSpinLock
(自旋锁) -> os_unfair_lock
(互斥锁) ->dispatch_semaphore_t
(信号量) -> pthread_mutex
(互斥锁) -> NSLock
(互斥锁) -> NSCondition
(条件锁) -> pthread_mutex_recursive
(互斥递归锁) -> NSRecursiveLock
(递归锁) -> NSConditionLock
(条件锁) -> synchronized
(互斥锁)
自旋锁线程安全吗?
@property (atomic, strong) NSArray *array; //Thread A dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i = 0; i < 100000; i ++) { if (i % 2 == 0) { self.array = @[@"Hank", @"CC", @"Cooci"]; } else { self.array = @[@"Kody"]; } } }); //Thread B dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i = 0; i < 100000; i ++) { if (self.array.count >= 2) { NSString* str = [self.array objectAtIndex:1]; } } }); 复制代码
分析:array是一个用自旋锁atomic修饰的属性,在多线程环境下会发生数组越界的奔溃。线程B调用[self.array objectAtIndex:1]时很有可能线程A已经调用了self.array = @[@"Kody"],此时就会发生数组越界的奔溃,这里就需要使用读写锁了。
如何实现一个读写锁
首先分析一下读写锁的特性
多读单写:多条线程读,单条线程写
写入和写入互斥,不能同时写,在分析gcd的时候我们使用了栅栏函数保证写的唯一性
写和读互斥,写的时候不能读。
写不能堵塞主线程
- (void)viewDidLoad { [super viewDidLoad]; self.dic=[[NSMutableDictionary alloc]init]; //自定义并发队列,栅栏函数必须自定义并发 queue=dispatch_queue_create("ttt", DISPATCH_QUEUE_CONCURRENT); [self gy_safeSetter:@"gg"]; [self gy_safeSetter:@"bb"]; [self gy_safeSetter:@"tt"]; [self gy_safeSetter:@"mm"]; } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ //模拟多线程读 for(int i=0;i<20;i++){ dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self gy_safeGetter]; }); } } //栅栏函数堵塞写,保证每次只有一个写入 -(void)gy_safeSetter:(NSString*)name{ __weak typeof(self) weakself=self; dispatch_barrier_async(queue, ^{ [weakself.dic setValue:name forKey:@"gy"]; NSLog(@"写入成功%@",name); }); } //多线程多读,dispatch_sync堵塞的是当前线程,没法堵塞另外一条线程,如果换成dispatch_async,那么当前线程就是异步,result还没设置值就return了,为什么不直接 result=[weakself.dic objectForKey:@"gy"],为了读写互斥 -(NSString*)gy_safeGetter{ __weak typeof(self) weakself=self; __block NSString* result; dispatch_sync(queue, ^{ result=[weakself.dic objectForKey:@"gy"]; }); NSLog(@"%@",result); return result; } 复制代码
分析:
首先看
gy_safeGetter
读的方法是如何实现多线程读
的。使用dispatch_sync
堵塞的是当前线程
,没法堵塞另外一条线程,如果换成dispatch_async
,那么当前线程就是异步,result还没设置值就return
了,为什么不直接result=[weakself.dic objectForKey:@"gy"]返回,那是为了保证在栅栏函数
中实现读写互斥
,栅栏函数的意义就是等待前面的事务完成再实现栅栏里面的事务。然后看
gy_safeSetter
写方法,使用栅栏函数实现写写互斥
,保证只有一个在写,其他线程等待写完之后再写。注意栅栏函数必须是自定义的并发队列
,否则这个栅栏函数的作用等同于一个同步函数的作用
作者:顶风尿一丈
链接:https://juejin.cn/post/7021431016866709518