1、 线程安全
1.1 线程安全的本质
线程安全:不是指线程的安全,而是指内存的安全。每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存),当多个线程访问该区域,这就是造成线程不安全的本质原因。
1.2 为什么会出现多线程不安全
针对同一块区域,我们有两种操作,读(load)和写(store),读和写同时发生在同一块区域的时候,就有可能出现多线程不安全
2、 课前预习
2.1 多线程下如何访问内存
多线程是如何同时访问内存的。不考虑CPU cache对变量的缓存,内存访问可以简单用下图表示
从上图中可以看出,我们只有一个地址总线,一个内存。即使是在多线程的环境下,也不可能存在两个线程同时访问同一块内存区域的场景,内存的访问一定是通过一个地址总线串行排队访问的。
- 最终结论
- 内存的访问是串行的,并不会导致内存数据的错乱或者应用的crash。
- 如果读写(load or store)的内存长度小于等于地址总线的长度,那么读写的操作是原子的,一次完成。比如bool,int,long在64位系统下的单次读写都是原子操作
2.2 从property看安全隐患
上面的图是什么意思呢?看个列子
@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、 多线程不安全的场景
平时项目中遇到的多线程问题主要是两类
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 好的读写安全设计原则
在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];
});
}
}
参考文章: