专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

iOS中如何设计多线程的读写安全

1、 线程安全

1.1 线程安全的本质

线程安全:不是指线程的安全,而是指内存的安全。每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存),当多个线程访问该区域,这就是造成线程不安全的本质原因。

1.2 为什么会出现多线程不安全

针对同一块区域,我们有两种操作,读(load)和写(store),读和写同时发生在同一块区域的时候,就有可能出现多线程不安全

2、 课前预习

2.1 多线程下如何访问内存

多线程是如何同时访问内存的。不考虑CPU cache对变量的缓存,内存访问可以简单用下图表示

75_1.png从上图中可以看出,我们只有一个地址总线,一个内存。即使是在多线程的环境下,也不可能存在两个线程同时访问同一块内存区域的场景,内存的访问一定是通过一个地址总线串行排队访问的。

  • 最终结论
    1. 内存的访问是串行的,并不会导致内存数据的错乱或者应用的crash。
    2. 如果读写(load or store)的内存长度小于等于地址总线的长度,那么读写的操作是原子的,一次完成。比如bool,int,long在64位系统下的单次读写都是原子操作

2.2 从property看安全隐患

75_2.png上面的图是什么意思呢?看个列子

@property (nonatomic, strong) NSString* userName;
@property (nonatomic, assign) NSInteger age;
//对象类型,但是userName属于TagPointer,在栈上,不存在线程安全问题
self.userName = @"pea";
//对象类型,userName属于指针类型,指向堆,多线程下可能会存在线程安全问题
self.userName = @"2323sdhfjshfdfsdfsdfsfd";
//栈上,不存在线程安全问题
self.age = 15 

访问userName的时候,访问的有可能是userName本身(TagPointer),也有可能是userName所指向的内存区域.

  • 总结: 只有当属性的类型是对象类型,并且指向了堆内存(指针类型),再多线程情况下才可能存在安全的问题

3、 多线程不安全的场景

平时项目中遇到的多线程问题主要是两类

75_3.png

3.1 数据访问出错

@property (atomic, assign)    int       intA;
//thread A
for (int i = 0; i < 10000; i ++) {
    self.intA = self.intA + 1;
    NSLog(@"Thread A: %d\n", self.intA);
}
//thread B
for (int i = 0; i < 10000; i ++) {
    self.intA = self.intA + 1;
    NSLog(@"Thread B: %d\n", self.intA);
}

结果分析:

  • 虽然intA声明为atomic,最后的结果也不一定会是20000。原因就是因为self.intA = self.intA + 1;不是原子操作
  • 虽然intA的getter和setter是原子操作,但当我们使用intA的时候,整个语句并不是原子的,这行赋值的代码至少包含读取(load),+1(add),赋值(store)三步操作
  • 当前线程store的时候可能其他线程已经执行了若干次store了,导致最后的值小于预期值

3.2 方法调用crash

property (nonatomic, strong) NSString* userName;

对于对象类型,在MRC时代,系统默认生成的setter类似如下

- (void)setUserName:(NSString *)userName {
    if(_uesrName != userName) {
        [userName retain];
       _userName = [_userName release]
    }
}

可以假设一种场景,线程1先通过getter获取当前_userName,之后线程2通过setter调用[_userName release];,线程1所持有的_userName就变成无效的地址空间了,如果再给这个地址空间发消息就会导致crash

4、 如何设计多线程的读写安全

有了以上知识的铺垫,我们来考虑下如何优雅的设计一个多现场的读写安全,这里以NSMutableArray来举列。

4.1 好的读写安全设计原则

75_4.png在YYKit,对于可变数组/字典的线程安全保护主要是通过semaphore来实现的,但是有个潜在的问题是:限制了数组的多线程读取操作

4.2 实现方案一【读写锁】

读写锁即: pthread_rwlock

查看实现方案testPthreadRWLock

-(void)testPthreadRWLock{
    _index = 0;
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (NSInteger i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            NSString *threadMsg = [NSString stringWithFormat:@"%@", [NSThread currentThread]];
            [self pthread_write:i threadMsg:threadMsg];
        });
        dispatch_async(queue, ^{
            NSString *threadMsg = [NSString stringWithFormat:@"%@", [NSThread currentThread]];
            [self pthread_read:threadMsg];
        });
        dispatch_async(queue, ^{
            NSString *threadMsg = [NSString stringWithFormat:@"%@", [NSThread currentThread]];
            [self pthread_read:threadMsg];
        });
        dispatch_async(queue, ^{
            NSString *threadMsg = [NSString stringWithFormat:@"%@", [NSThread currentThread]];
            [self pthread_read:threadMsg];
        });
    }
}

- (void)pthread_write:(NSInteger)index threadMsg:(NSString *)threadMsg {
    pthread_rwlock_wrlock(&_lock);
    [self write:index threadMsg:threadMsg];//这里方法就不具体写了
    pthread_rwlock_unlock(&_lock);
}
- (void)pthread_read:(NSString *)threadMsg {
    pthread_rwlock_rdlock(&_lock);
    [self read:threadMsg];
    pthread_rwlock_unlock(&_lock);
}

4.3 实现方案二 【栅栏+并发队列】

1、 创建并发队列

self.concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

  • 传入的并发队列【必须】是自己通过【dispatch_queue_cretate】创建的
  • 如果传入的是一个【串行队列】或【全局并发队列】,那这个函数便等同于【dispatch_async/dispatch_sync】函数的效果

1、 读操作

  • 可以用dispatch_async或者dispatch_sync,推荐用dispatch_sync
  • 使用 dispatch_sync 函数是确保在获取到要读取的对象之前,当前线程是阻塞的,也即确保如果此时有写入操作,则需要等到写读操作完成后才能执行,而读取操作是不会 barrier 队列,可以多线程并行。(这里可能会有点绕,可以多思考下)
- (void)barrierSYNC_read:(NSString *)threadMsg {
    dispatch_sync(self.concurrentQueue, ^{
        [self read:threadMsg];
    });
}

1、 写操作
使用 dispatch_barrier_async 函数,确保两点:一是在执行此任务之前队列中其他任务已经完成,二是此任务完成之前队列中新增的任务不会执行,达到 barrier 的目标

- (void)barrierSYNC_write:(NSInteger)index threadMsg:(NSString *)threadMsg {
    dispatch_barrier_async(self.concurrentQueue, ^{
        [self write:index threadMsg:threadMsg];
    });
}

1、 测试

-(void)testPthreadRWLock{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (NSInteger i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            NSString *threadMsg = [NSString stringWithFormat:@"%@", [NSThread currentThread]];
            [self barrierSYNC_write:i threadMsg:threadMsg];
        });
        dispatch_async(queue, ^{
            NSString *threadMsg = [NSString stringWithFormat:@"%@", [NSThread currentThread]];
            [self barrierSYNC_read:threadMsg];
        });
        dispatch_async(queue, ^{
            NSString *threadMsg = [NSString stringWithFormat:@"%@", [NSThread currentThread]];
            [self barrierSYNC_read:threadMsg];
        });
    }
 }

参考文章:

文章永久链接:https://tech.souyunku.com/31631

未经允许不得转载:搜云库技术团队 » iOS中如何设计多线程的读写安全

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们