这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
目录如下:
- iOS 底层原理探索 之 alloc
- iOS 底层原理探索 之 结构体内存对齐
- iOS 底层原理探索 之 对象的本质 & isa的底层实现
- iOS 底层原理探索 之 isa - 类的底层原理结构(上)
- iOS 底层原理探索 之 isa - 类的底层原理结构(中)
- iOS 底层原理探索 之 isa - 类的底层原理结构(下)
- iOS 底层原理探索 之 Runtime运行时&方法的本质
- iOS 底层原理探索 之 objc_msgSend
- iOS 底层原理探索 之 Runtime运行时慢速查找流程
- iOS 底层原理探索 之 动态方法决议
- iOS 底层原理探索 之 消息转发流程
- iOS 底层原理探索 之 应用程序加载原理dyld (上)
- iOS 底层原理探索 之 应用程序加载原理dyld (下)
- iOS 底层原理探索 之 类的加载
- iOS 底层原理探索 之 分类的加载
- iOS 底层原理探索 之 关联对象
- iOS底层原理探索 之 魔法师KVC
- iOS底层原理探索 之 KVO原理|8月更文挑战
- iOS底层原理探索 之 重写KVO|8月更文挑战
- iOS底层原理探索 之 多线程原理|8月更文挑战
- iOS底层原理探索 之 GCD函数和队列
- iOS底层原理探索 之 GCD原理(上)
- iOS底层 - 关于死锁,你了解多少?
- iOS底层 - 单例 销毁 可否 ?
- iOS底层 - Dispatch Source
- iOS底层 - 一个栅栏函 拦住了 数
- iOS底层 - 不见不散 的 信号量
- iOS底层 GCD - 一进一出 便成 调度组
- iOS底层原理探索 - 锁的基本使用
- iOS底层 - @synchronized 流程分析
- iOS底层 - 锁的原理探索
以上内容的总结专栏
细枝末节整理
前言
前面的内容中,我们通过 对锁的基本使用开篇讲解各种锁的基本使用事项; 而后 对 开发中使用起来很方便的 @synchronized 这把锁的底层结构、流程进行了一个探索; 最后,我们对 NSLock、 NSRecursiveLock 以及 NSCondition、NSConditionLock的流程探索。通过这三篇,我们对于iOS开发中的锁的原理及其使用有了更深一层的理解。 今天,我们对 之前文章中 没有涉及的 读写锁 自己实现一下。 好了,这就开始今天的内容吧。
读写锁要求
要实现读写锁,我们先思考下,如果我们要用一把读写锁,你希望这把锁可以满足哪些要求?
- 首先,需要满足 可以多线程、 多读单写;
- 其次,要满足 读写互斥、 写写互斥;
- 最后,读写的操作 不可以阻塞到当前线程;
读写锁实现
基于上面三点要求,我们需要总结下,之前总结的知识点内容,结合GCD和锁的知识做一下选型,看哪些点儿 可以满足我们的要求,有机会作为具体的实现内容。
技术选型
信号量(dispatch_semaphore_t) 可以控制最大并发数,且dispatch_semaphore_signal
和 dispatch_semaphore_wait
的配合使用可以实现同步,控制流程的效果。但是,当使用多线程的时候,就算我们控制了最大并发数为1,我们是无法判断哪一个先被点用,所以,对于等待和发送信号,是无从下手的,所以,信号量不行。
栅栏函数(dispatch_barrier_async、dispatch_barrier_sync)最直接的作用 就是 控制 任务执行顺序,同步;只能控制同一并发队列。同样入选。
调度组(dispatch_group_t)调度组最直接的作用就是 控制任务执行顺序,只要enter和leave配合合适就可以并发执行,所以并不符合要求。 还有就是 Dispatch Source 实际使用中并不多,不过可以实现自定义Timer , pass 不符合要求。
好,有了两个思路,我们就尝试来实现读写锁。
Demo实现
首先定义一个本次我们被读写的对象 LockTest
:
@interface LockTest : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *like;
@end
接着,我们要自定义一个 并发队列 :
@property (nonatomic, strong) dispatch_queue_t safeQueue;
...
_cusQueue = dispatch_queue_create("rw.test.queue", DISPATCH_QUEUE_CONCURRENT);
下面就是本次的重点,读写 内容的实现,读我们通过 dispatch_barrier_async
来实现 :
- (NSObject *)safeReadPro:( NSString *)pro
andObject:( NSObject *)obj {
__block NSObject *a;
dispatch_barrier_async(_safeQueue, ^{
NSLog(@"开始读取--");
sleep( ( arc4random() % 2 ) );
a = [obj valueForKey:pro];
NSLog(@"读取--%@--%@", a , [NSThread currentThread]);
});
NSLog(@"%@", a);
return a;
}
写入,我们通过 dispatch_barrier_async
来实现,因为是栅栏函数,所以写与写之间是互斥的,符合要求:
- (void)safeSetPro:( NSString *)pro
andNewPro:( NSObject *)nPro
andObject:( NSObject *)obj {
dispatch_barrier_async(_safeQueue, ^{
NSLog(@"开始写入--");
sleep( ( arc4random() % 2 ) );
[obj setValue:nPro forKey:pro];
NSLog(@"写入--%@--%@", [obj valueForKey:pro], [NSThread currentThread]);
});
}
请注意,很细节的哦,我们有模拟延迟的操作。
接下来,开始测试 :
哎呦,打印了一大串的 null ;
(null)
(null)
(null)
(null)
(null)
(null)
(null)
(null)
(null)
(null)
(null)
(null)
(null)
(null)
(null)
(null)
(null)
(null)
(null)
(null)
为什么呢?
找到问题了, 因为我们读取的时候,用的 dispatch_barrier_async
还没有读取完,就 return
。
那么,怎么解决呢?
换成 dispatch_barrier_sync
就好了,很简单, 并且 如果这里我们不通过 dispatch_barrier_sync
来实现,直接取值然后返回的,那么就无法实现读写之间的互斥。会造成,多线程我们不断的在赋值,然而,我们读到了数据就是不一定哪一个值了,加上 dispatch_barrier_sync
之后,在读的时候,就无法写,写的时候,就无法读,这样,完美的实现了读写互斥。
接着在运行项目 , 看日志 :
开始写入--
写入--superman-0--<NSThread: 0x600003479840>{number = 7, name = (null)}
开始读取--
读取--(null)--<NSThread: 0x60000347a740>{number = 6, name = (null)}
(null)
开始读取--
读取--superman-0--<NSThread: 0x600003418140>{number = 8, name = (null)}
superman-0
开始写入--
写入--superman-nick-0--<NSThread: 0x60000347a740>{number = 6, name = (null)}
开始写入--
写入--superman-nick-1--<NSThread: 0x60000347a740>{number = 6, name = (null)}
开始读取--
读取--superman-nick-1--<NSThread: 0x600003429380>{number = 5, name = (null)}
superman-nick-1
开始读取--
读取--superman-0--<NSThread: 0x600003432dc0>{number = 3, name = (null)}
superman-0
开始写入--
写入--superman-1--<NSThread: 0x60000347a740>{number = 6, name = (null)}
开始写入--
写入--superman-nick-2--<NSThread: 0x60000347a740>{number = 6, name = (null)}
开始读取--
读取--superman-1--<NSThread: 0x60000347ad80>{number = 9, name = (null)}
superman-1
开始写入--
写入--superman-2--<NSThread: 0x60000347a740>{number = 6, name = (null)}
开始读取--
读取--superman-nick-2--<NSThread: 0x600003429a00>{number = 4, name = (null)}
superman-nick-2
开始写入--
写入--superman-3--<NSThread: 0x60000347ad80>{number = 9, name = (null)}
开始写入--
写入--superman-nick-3--<NSThread: 0x60000347ad80>{number = 9, name = (null)}
开始读取--
读取--superman-nick-3--<NSThread: 0x600003434800>{number = 10, name = (null)}
superman-nick-3
开始读取--
读取--superman-3--<NSThread: 0x60000347adc0>{number = 11, name = (null)}
superman-3
开始写入--
写入--superman-4--<NSThread: 0x600003434800>{number = 10, name = (null)}
开始写入--
写入--superman-nick-4--<NSThread: 0x600003434800>{number = 10, name = (null)}
开始读取--
读取--superman-4--<NSThread: 0x60000347a640>{number = 12, name = (null)}
superman-4
开始读取--
读取--superman-nick-4--<NSThread: 0x600003436fc0>{number = 13, name = (null)}
superman-nick-4
开始写入--
写入--superman-5--<NSThread: 0x60000347a640>{number = 12, name = (null)}
开始读取--
读取--superman-5--<NSThread: 0x60000347a740>{number = 14, name = (null)}
superman-5
开始写入--
写入--superman-nick-5--<NSThread: 0x60000347a640>{number = 12, name = (null)}
开始写入--
写入--superman-6--<NSThread: 0x60000347a640>{number = 12, name = (null)}
开始读取--
读取--superman-nick-5--<NSThread: 0x600003432cc0>{number = 15, name = (null)}
superman-nick-5
开始写入--
写入--superman-nick-6--<NSThread: 0x60000347a640>{number = 12, name = (null)}
开始读取--
读取--superman-6--<NSThread: 0x6000034705c0>{number = 16, name = (null)}
superman-6
开始读取--
读取--superman-nick-6--<NSThread: 0x600003418580>{number = 17, name = (null)}
superman-nick-6
开始写入--
写入--superman-7--<NSThread: 0x6000034705c0>{number = 16, name = (null)}
开始读取--
读取--superman-7--<NSThread: 0x60000343c500>{number = 18, name = (null)}
superman-7
开始写入--
写入--superman-nick-7--<NSThread: 0x6000034705c0>{number = 16, name = (null)}
开始读取--
读取--superman-nick-7--<NSThread: 0x60000347aa40>{number = 19, name = (null)}
superman-nick-7
开始写入--
完美,一个读写锁,我们就完成了。