读写锁场景:

同一时间,只能有1个线程进行写的操作

同一时间,允许有多个线程进行读的操作

同一时间,不允许既有写的操作,又有读的操作

 上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作,iOS中的实现方案有:

1、读写锁:pthread_rwlock

等待锁的线程会进入休眠

// 导入头文件
#import <pthread.h>

// 声明属性
@property (nonatomic, assign) pthread_rwlock_t rwlock;


// 初始化
pthread_rwlock_init(&_rwlock, NULL);
 
// 读加锁
- (void)read {
    pthread_rwlock_rdlock(&_rwlock);
    NSLog(@"read");
    pthread_rwlock_unlock(&_rwlock);
}
 
// 写加锁
- (void)wtite {
    pthread_rwlock_wrlock(&_rwlock);
    NSLog(@"write");
    pthread_rwlock_unlock(&_rwlock);
}

// 销毁锁
- (void)dealloc {
    pthread_rwlock_destroy(&_rwlock);
}


2、dispatch_barrier_async

这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的,全局队列不可以

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) dispatch_queue_t concurrentQueue;

@end


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self readWriteLock];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self readWriteLock];
    // 重新生成一个vc,验证当前vc是否会产生循环引用,导致无法释放
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        UIViewController *vc = [[UIViewController alloc] init];
        vc.view.backgroundColor = [UIColor redColor];
        [UIApplication sharedApplication].keyWindow.rootViewController = vc;
    });

}

- (void)readWriteLock {
    // 使用自己创建的并发队列
    self.concurrentQueue = dispatch_queue_create("aaa", DISPATCH_QUEUE_CONCURRENT);
    // 使用全局队列,必定野指针崩溃
//    self.concurrentQueue = dispatch_get_global_queue(0, 0);

    // 测试代码,模拟多线程情况下的读写
    for (int i = 0; i<10; i++) {
        // 创建10个线程进行写操作
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self updateText:[NSString stringWithFormat:@"噼里啪啦--%d",i]];
        });

    }

    for (int i = 0; i<50; i++) {
        // 50个线程进行读操作
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"读 %@ %@",[self getCurrentText],[NSThread currentThread]);
        });

    }

    for (int i = 10; i<20; i++) {
        // 10个进行写操作
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self updateText:[NSString stringWithFormat:@"噼里啪啦--%d",i]];
        });

    }
}

// 写操作,栅栏函数是不允许并发的,所以"写操作"是单线程进入的,根据log可以看出来
- (void)updateText:(NSString *)text {
    // block内不需要使用weakSelf, 不会产生循环引用
    dispatch_barrier_async(self.concurrentQueue, ^{
        self.text = text;
        NSLog(@"写操作 %@ %@",text,[NSThread currentThread]);
        // 模拟耗时操作,打印log可以放发现是1个1个执行,没有并发
        sleep(1);
    });
}
// 读操作,这个是可以并发的,log在很快时间打印完
- (NSString *)getCurrentText {
    __block NSString * t = nil;
    // block内不需要使用weakSelf, 不会产生循环引用
    dispatch_sync(self.concurrentQueue, ^{
        t = self.text;
        // 模拟耗时操作,瞬间执行完,说明是多个线程并发的进入的
        sleep(1);
    });
    return t;
}

- (void)dealloc {
    // 页面可以正常释放
    NSLog(@"%s",__FUNCTION__);
}


@end

参考Effective Objective-C . 书上用了全局队列, 实测全局队列的栅栏是无效的, 必定会导致野指针.算是书上的一个小bug吧.

下面在贴上一个AFN的实现 , requestHeaderModificationQueue是一个由AFHTTPRequestSerializer创建的并发队列, 使用这个并发队列对一个NSMutableDictionary实现了多读单写的锁.

    /// 并发队列
    self.requestHeaderModificationQueue = dispatch_queue_create("requestHeaderModificationQueue", DISPATCH_QUEUE_CONCURRENT);


对于读写锁的3个要求,  前2个都可以通过一个普通的锁来实现, 比如就通过NSLock在写的时候加锁, 这样可以满足条件1, 多个线程读的话, 那就不加锁来处理. 看上去一个普通的lock就可以满足"多读单写"的要求了, 那为什么还需要要求同一时间不能既有读操作,也有写操作呢?

  • 条件1: 同一时间,只能有1个线程进行写的操作
  • 条件2: 同一时间,允许有多个线程进行读的操作
  • 条件3: 同一时间,不允许既有写的操作,又有读的操作

我们使用这段代码测试下,  

- (void)personDicTest {
    NSLock *lock = [[NSLock alloc] init];

    NSMutableDictionary * dic = [NSMutableDictionary dictionary];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        while (1) {
            [lock lock];
            dic[@"1"] = [[Person alloc] init];
            [lock unlock];
        }

    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            Person *p = [dic objectForKey:@"1"];
            NSLog(@"%@",p);
        }
    });
}

发现的确是不可以的, 只在写的时候加锁, 读的时候不加锁, 会发生crash,发生概率大约是1%, 前面的NSLog正常打印了97次, 在第98次打印时发生了野指针错误. 由于开启xcode的编译选项, 控制台输出了一条信息,给一个已经释放的对象发送了消息. 标准的野指针错误.

-[Person isProxy]: message sent to deallocated instance 0x107d75ac0

 如果在读操作时也加上锁, 那么就不会有问题了, 但是这样就不满足 "多读单写"的要求了,

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            [lock lock];
            Person *p = [dic objectForKey:@"1"];
            NSLog(@"%@",p);
            [lock unlock];
        }
    });

所以想要满足"同一时间多读单写"的读写锁,方案只有pthread_rwlock/dispatch_barrier_async, 普通的锁无法满足要求.


上面的测试又带了一个新问题, 通过实验知道只在写操作加锁不能满足要求, 那为什么不能满足要求呢? 是什么导致的会crash呢?

分析下读写操作中都发生了什么?

写操作, 线程A : NSLock加锁 -- 从字典中找出旧值 -- 调用旧值的release方法 -- 设置新值 -- 调用新值的 retain方法, 增加引用计数 -- NSLock解锁

读操作, 线程B : 从字典中查到key为@"1"的对象地址 -- 对此地址进行一次retain -- 调用[obj autoRelease] 返回此地址 -- 外界使用局部变量 *p接受此地址 -- 调用一次 [p retain] -- 使用此值做事情,.... -- 局部变量作用域消失, 调用 [p release]

 由于读操作没有加锁, 2个线程可能会同时执行, 那么就有可能发生 线程B先从地址中取出值, 线程A调用release方法, 这个时候这个对象的引用计数就已经是0, 这个地址是野指针了.

如果给读操作加上锁, 那么这部分代码就不会有多线程进入了, 也就不会发生野指针了. 看似锁里只有一行代码, 但是在运行时拆解成了很多汇编指令.

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐