iOS底层 - 带你实现一个读写锁

1,653 阅读7分钟

这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战

写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend
  9. iOS 底层原理探索 之 Runtime运行时慢速查找流程
  10. iOS 底层原理探索 之 动态方法决议
  11. iOS 底层原理探索 之 消息转发流程
  12. iOS 底层原理探索 之 应用程序加载原理dyld (上)
  13. iOS 底层原理探索 之 应用程序加载原理dyld (下)
  14. iOS 底层原理探索 之 类的加载
  15. iOS 底层原理探索 之 分类的加载
  16. iOS 底层原理探索 之 关联对象
  17. iOS底层原理探索 之 魔法师KVC
  18. iOS底层原理探索 之 KVO原理|8月更文挑战
  19. iOS底层原理探索 之 重写KVO|8月更文挑战
  20. iOS底层原理探索 之 多线程原理|8月更文挑战
  21. iOS底层原理探索 之 GCD函数和队列
  22. iOS底层原理探索 之 GCD原理(上)
  23. iOS底层 - 关于死锁,你了解多少?
  24. iOS底层 - 单例 销毁 可否 ?
  25. iOS底层 - Dispatch Source
  26. iOS底层 - 一个栅栏函 拦住了 数
  27. iOS底层 - 不见不散 的 信号量
  28. iOS底层 GCD - 一进一出 便成 调度组
  29. iOS底层原理探索 - 锁的基本使用
  30. iOS底层 - @synchronized 流程分析
  31. 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
 开始写入--

完美,一个读写锁,我们就完成了。