概述
为什么会有数组的线程安全问题?
对于可变的集合(NSMutableArray、NSMutableDictionary、NSMutableSet)是可读可写的,所以有可能出现这种场景:两个或多个线程同时对一个可变集合进行读、写、新增及删除的操作,这样是得不到预期的结果的,甚至程序会抛出异。如果我们将这些线程用一定的规则去管理好,那就可以解决这个问题了。下面我们开始着手处理这个问题吧
先修
要解决这个线程安全的问题,需要明白两个知识点
1.nonatomic 和atomic
这两个关键字是用来修饰成员变量的。前者是非原子操作即线程可以随便访问成员变量,后者是原子操作即线程访问按照一定的规则进行。
nonatomic:
如果只存在单个线程访问成员变量,用它修饰是非常不错的,因为没有对访问进行线程加锁,效率非常高。但是正因为没有加锁,所以可能同时进行读写,导致不可预期的错误。
atomic:
用atomic修饰成员变量,会给成员变量的getter 和 setter方法加锁,使访问每次只能进行一个,避免多个线程同时操作成员变量,所以适用于多线程访问成员变量的场景。
虽然atomic修饰的成员变量在多线程去访问时不会出现错误,但结果不一定准确:
比如说有一个成员变量name,当a线程去getter name的值,同时有b线程和c线程对name 进行setter值,那么name的值就不确定了,可能是b线程操作之前的值,也有可能是b线程操作之后的值,也有可能是c线程操作之后的值。
再看下面这段代码:
- (void)competition {
self.count = 0;
dispatch_async(queue1, ^{
for (int i = 0; i < 10000; i++) {
self.count = self.count + 1;
}
});
dispatch_async(queue2, ^{
for (int i = 0; i < 10000; i++) {
self.count = self.count + 1;
}
});
}
上面这段代码的最终结果肯定小于20000。当获取值的时候都是原子线程安全操作,比如两个线程都获取了当前值 0,于是分别增量后变为了 1,所以两个队列依序写入值都是 1,所以不是线程安全的。
2.dispatch_barrier_async 和dispatch_barrier_sync
这是GDC里面的两个栅栏方法,需要配合队列使用。其作用是拦住前面添加到队列的任务,让这些任务执行完成,然后再执行栅栏里的任务,两个方法的区别是:
- dispatch_barrier_async不阻塞主线程;
- dispatch_barrier_sync阻塞主线程,非得等到栅栏里的任务执行完成程序才能执行主线程的任务。
- 另外一点需要明确的是,栅栏函数只对主队列和自身所在队列有影响,其他队列不受影响。
如果在队列中的栅栏之后再添加任务,则此任务要等到栅栏里的任务完成后才会执行。
看一段代码就一目了然了
先使用 dispatch_barrier_sync
dispatch_queue_t concurrent_queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrent_queue, ^{
for (int i = 0; i < 500; i++) {
if (i % 100 == 0) {
NSLog(@"任务一%d",i);
}
}
});
dispatch_async(concurrent_queue, ^{
for (int i = 0; i < 50; i++) {
if (i % 10 == 0) {
NSLog(@"任务二%d",i);
}
}
});
dispatch_async(concurrent_queue, ^{
for (int i = 0; i < 30; i++) {
if (i % 5 == 0) {
NSLog(@"任务三%d",i);
}
}
});
// 这里使用同步栅栏函数
dispatch_barrier_sync(concurrent_queue, ^{
for (int i = 0; i < 40; i++) {
if (i % 5 == 0) {
NSLog(@"-------同步barrier的任务%d-------",i);
}
}
});
NSLog(@"外面的任务");
dispatch_async(concurrent_queue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"任务四%d",i);
}
});
dispatch_async(concurrent_queue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"任务六%d",i);
}
});
打印结果如下
2019-07-31 17:22:17.599644+0800 ArrayTest[14396:408598] 任务一0
2019-07-31 17:22:17.599644+0800 ArrayTest[14396:408597] 任务三0
2019-07-31 17:22:17.599644+0800 ArrayTest[14396:408596] 任务二0
2019-07-31 17:22:17.599823+0800 ArrayTest[14396:408598] 任务一100
2019-07-31 17:22:17.599823+0800 ArrayTest[14396:408597] 任务三5
2019-07-31 17:22:17.599824+0800 ArrayTest[14396:408596] 任务二10
2019-07-31 17:22:17.599931+0800 ArrayTest[14396:408597] 任务三10
2019-07-31 17:22:17.599949+0800 ArrayTest[14396:408598] 任务一200
2019-07-31 17:22:17.599932+0800 ArrayTest[14396:408596] 任务二20
2019-07-31 17:22:17.600011+0800 ArrayTest[14396:408597] 任务三15
2019-07-31 17:22:17.600252+0800 ArrayTest[14396:408598] 任务一300
2019-07-31 17:22:17.600424+0800 ArrayTest[14396:408597] 任务三20
2019-07-31 17:22:17.600626+0800 ArrayTest[14396:408598] 任务一400
2019-07-31 17:22:17.600784+0800 ArrayTest[14396:408597] 任务三25
2019-07-31 17:22:17.601275+0800 ArrayTest[14396:408596] 任务二30
2019-07-31 17:22:17.601423+0800 ArrayTest[14396:408596] 任务二40
2019-07-31 17:22:17.601702+0800 ArrayTest[14396:408489] -------同步barrier的任务0-------
2019-07-31 17:22:17.601942+0800 ArrayTest[14396:408489] -------同步barrier的任务5-------
2019-07-31 17:22:17.602155+0800 ArrayTest[14396:408489] -------同步barrier的任务10-------
2019-07-31 17:22:17.602368+0800 ArrayTest[14396:408489] -------同步barrier的任务15-------
2019-07-31 17:22:17.602592+0800 ArrayTest[14396:408489] -------同步barrier的任务20-------
2019-07-31 17:22:17.602798+0800 ArrayTest[14396:408489] -------同步barrier的任务25-------
2019-07-31 17:22:17.603012+0800 ArrayTest[14396:408489] -------同步barrier的任务30-------
2019-07-31 17:22:17.616610+0800 ArrayTest[14396:408489] -------同步barrier的任务35-------
2019-07-31 17:22:17.616736+0800 ArrayTest[14396:408489] 外面的任务
2019-07-31 17:22:17.616874+0800 ArrayTest[14396:408598] 任务六0
2019-07-31 17:22:17.616899+0800 ArrayTest[14396:408597] 任务四0
2019-07-31 17:22:17.617111+0800 ArrayTest[14396:408598] 任务六1
2019-07-31 17:22:17.617199+0800 ArrayTest[14396:408597] 任务四1
2019-07-31 17:22:17.617345+0800 ArrayTest[14396:408598] 任务六2
2019-07-31 17:22:17.617427+0800 ArrayTest[14396:408597] 任务四2
再使用dispatch_barrier_async
// 这里使用异步栅栏函数
dispatch_barrier_async(concurrent_queue, ^{
for (int i = 0; i < 40; i++) {
if (i % 5 == 0) {
NSLog(@"-------异步barrier的任务%d-------",i);
}
}
});
打印结果如下:
2019-07-31 17:25:28.130839+0800 ArrayTest[14457:413975] 任务一0
2019-07-31 17:25:28.130846+0800 ArrayTest[14457:413977] 任务三0
2019-07-31 17:25:28.130839+0800 ArrayTest[14457:413986] 任务二0
2019-07-31 17:25:28.131042+0800 ArrayTest[14457:413977] 任务三5
2019-07-31 17:25:28.131067+0800 ArrayTest[14457:413986] 任务二10
2019-07-31 17:25:28.131043+0800 ArrayTest[14457:413975] 任务一100
2019-07-31 17:25:28.131130+0800 ArrayTest[14457:413977] 任务三10
2019-07-31 17:25:28.131157+0800 ArrayTest[14457:413975] 任务一200
2019-07-31 17:25:28.131172+0800 ArrayTest[14457:413986] 任务二20
2019-07-31 17:25:28.131238+0800 ArrayTest[14457:413977] 任务三15
2019-07-31 17:25:28.130880+0800 ArrayTest[14457:413837] 外面的任务
2019-07-31 17:25:28.131664+0800 ArrayTest[14457:413975] 任务一300
2019-07-31 17:25:28.131828+0800 ArrayTest[14457:413977] 任务三20
2019-07-31 17:25:28.131980+0800 ArrayTest[14457:413975] 任务一400
2019-07-31 17:25:28.132137+0800 ArrayTest[14457:413977] 任务三25
2019-07-31 17:25:28.132620+0800 ArrayTest[14457:413986] 任务二30
2019-07-31 17:25:28.132911+0800 ArrayTest[14457:413986] 任务二40
2019-07-31 17:25:28.133144+0800 ArrayTest[14457:413986] -------异步barrier的任务0-------
2019-07-31 17:25:28.133334+0800 ArrayTest[14457:413986] -------异步barrier的任务5-------
2019-07-31 17:25:28.133543+0800 ArrayTest[14457:413986] -------异步barrier的任务10-------
2019-07-31 17:25:28.133761+0800 ArrayTest[14457:413986] -------异步barrier的任务15-------
2019-07-31 17:25:28.133959+0800 ArrayTest[14457:413986] -------异步barrier的任务20-------
2019-07-31 17:25:28.134183+0800 ArrayTest[14457:413986] -------异步barrier的任务25-------
2019-07-31 17:25:28.140504+0800 ArrayTest[14457:413986] -------异步barrier的任务30-------
2019-07-31 17:25:28.140658+0800 ArrayTest[14457:413986] -------异步barrier的任务35-------
2019-07-31 17:25:28.140785+0800 ArrayTest[14457:413986] 任务四0
2019-07-31 17:25:28.140788+0800 ArrayTest[14457:413977] 任务六0
2019-07-31 17:25:28.140883+0800 ArrayTest[14457:413986] 任务四1
2019-07-31 17:25:28.140892+0800 ArrayTest[14457:413977] 任务六1
2019-07-31 17:25:28.140961+0800 ArrayTest[14457:413986] 任务四2
2019-07-31 17:25:28.140987+0800 ArrayTest[14457:413977] 任务六2
实现线程安全的数组
通过上面的知识点可以知道,一个用nonatomic修饰的数组成员变量,它的线程访问是不受限制的,当然我们也已经知道用atomic修饰也并不合适,因为线程访问得到的值依然不够准确。
那要实现线程安全的数组,该怎么办呢?使用dispatch_barrier函数可以解决。
将数组的写(插入、修改、删除)操作放进队列中dispatch_barrier函数中,这样当进行写的操作时,会先等待前面的读的任务完成后再执行写操作;而且后面的读任务也要等待dispatch_barrier中的写操作执行完成后才会被执行。
代码
创建一个类给它添加一个可变数组的成员变量,给这个类添加访问数组成员变量的所有方法。不多说,看代码:
.h文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ZHMutableArray : NSObject
// 读取数组
- (NSMutableArray *)array;
//判断是否包含对象
- (BOOL)containsObject:(id)anObject;
//集合元素数量
- (NSUInteger)count;
//获取元素
- (id)objectAtIndex:(NSUInteger)index;
//枚举元素
- (NSEnumerator *)objectEnumerator;
//插入
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index;
//添加
- (void)addObject:(id)anObject;
//移除
- (void)removeObjectAtIndex:(NSUInteger)index;
//移除
- (void)removeObject:(id)anObject;
//移除
- (void)removeLastObject;
//替换
- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject;
//获取索引
- (NSUInteger)indexOfObject:(id)anObject;
@end
.m文件
凡涉及更改数组中元素的操作,使用异步栅栏块;读取数据使用 同步+并行队列
#import "ZHMutableArray.h"
@interface ZHMutableArray()
@property (nonatomic,strong)dispatch_queue_t concurrentQueue;
@property (nonatomic,strong)NSMutableArray *arr;
@end
@implementation ZHMutableArray
-(instancetype)init{
self = [super init];
if (self) {
NSString *identifier = [NSString stringWithFormat:@"<ZHMutableArray>%p",self];
self.concurrentQueue = dispatch_queue_create([identifier UTF8String], DISPATCH_QUEUE_CONCURRENT);
self.arr = [NSMutableArray array];
}
return self;
}
- (NSMutableArray *)array
{
__block NSMutableArray *safeArray;
dispatch_sync(_concurrentQueue, ^{
safeArray = self.arr;
});
return safeArray;
}
- (BOOL)containsObject:(id)anObject
{
__block BOOL isExist = NO;
dispatch_sync(_concurrentQueue, ^{
isExist = [self.arr containsObject:anObject];
});
return isExist;
}
- (NSUInteger)count
{
__block NSUInteger count;
dispatch_sync(_concurrentQueue, ^{
count = self.arr.count;
});
return count;
}
- (id)objectAtIndex:(NSUInteger)index
{
__block id obj;
dispatch_sync(_concurrentQueue, ^{
if (index < [self.arr count]) {
obj = self.arr[index];
}
});
return obj;
}
- (NSEnumerator *)objectEnumerator
{
__block NSEnumerator *enu;
dispatch_sync(_concurrentQueue, ^{
enu = [self.arr objectEnumerator];
});
return enu;
}
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index
{
dispatch_barrier_async(_concurrentQueue, ^{
if (anObject && index < [self.arr count]) {
[self.arr insertObject:anObject atIndex:index];
}
});
}
- (void)addObject:(id)anObject
{
dispatch_barrier_async(_concurrentQueue, ^{
if(anObject){
[self.arr addObject:anObject];
}
});
}
- (void)removeObjectAtIndex:(NSUInteger)index
{
dispatch_barrier_async(_concurrentQueue, ^{
if (index < [self.arr count]) {
[self.arr removeObjectAtIndex:index];
}
});
}
- (void)removeObject:(id)anObject
{
dispatch_barrier_async(_concurrentQueue, ^{
[self.arr removeObject:anObject];//外边自己判断合法性
});
}
- (void)removeLastObject
{
dispatch_barrier_async(_concurrentQueue, ^{
[self.arr removeLastObject];
});
}
- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject
{
dispatch_barrier_async(_concurrentQueue, ^{
if (anObject && index < [self.arr count]) {
[self.arr replaceObjectAtIndex:index withObject:anObject];
}
});
}
- (NSUInteger)indexOfObject:(id)anObject
{
__block NSUInteger index = NSNotFound;
dispatch_sync(_concurrentQueue, ^{
for (int i = 0; i < [self.arr count]; i ++) {
if ([self.arr objectAtIndex:i] == anObject) {
index = i;
break;
}
}
});
return index;
}
- (void)dealloc
{
if (_concurrentQueue) {
_concurrentQueue = NULL;
}
}
这样一个线程安全的数组就创建完成。
其他
针对网上一些博客说,使用atomic修饰的属性,在访问时会出现效率很低的情况,个人研究了下,觉得这事不靠谱,不知道读者有没有办法证明这个观点。
看下面的代码:
首先在.m文件中声明两个私有属性
@property (nonatomic,strong)ZHMutableArray *array;
@property (atomic,strong)NSMutableArray *tempAarray;
然后,往两个集合加入相同数量相同的值
self.array = [[ZHMutableArray alloc] init];
for (int i = 0; i < 10000; i++) {
NSString *str = [NSString stringWithFormat:@"%d",i];
[self.array addObject:str];
}
//
self.tempAarray = [[NSMutableArray alloc] init];
for (int i = 0; i < 10000; i++) {
[self.tempAarray addObject:[NSString stringWithFormat:@"%d",i]];
}
最后执行数组的访问
// tempArray的访问
dispatch_queue_t queue_t00 = dispatch_queue_create("dispatch_queue00", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue_t01 = dispatch_queue_create("dispatch_queue01", DISPATCH_QUEUE_CONCURRENT);
CFAbsoluteTime currentTime0 = CFAbsoluteTimeGetCurrent();
dispatch_apply(5000, queue_t00, ^(size_t index0) {
NSLog(@"%@",[self.tempAarray objectAtIndex:index0]);
});
dispatch_apply(5000, queue_t01, ^(size_t index0) {
NSLog(@"%@",[self.tempAarray objectAtIndex:index0+4999]);
});
CFAbsoluteTime totalTime0 = CFAbsoluteTimeGetCurrent() - currentTime0;
// array的访问
dispatch_queue_t queue_t10 = dispatch_queue_create("dispatch_queue10", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue_t11 = dispatch_queue_create("dispatch_queue11", DISPATCH_QUEUE_CONCURRENT);
CFAbsoluteTime currentTime1 = CFAbsoluteTimeGetCurrent();
dispatch_apply(5000, queue_t10, ^(size_t index1) {
NSLog(@"%@",[self.array objectAtIndex:index1]);
});
dispatch_apply(5000, queue_t11, ^(size_t index1) {
NSLog(@"%@",[self.array objectAtIndex:index1 + 4999]);
});
CFAbsoluteTime totalTime1 = CFAbsoluteTimeGetCurrent() - currentTime1;
// 两个访问时间对比
NSLog(@"totalTime0:%f - totalTime1:%f",totalTime0,totalTime1);
打印结果如下:
第一次:totalTime0:2.519118 - totalTime1:2.448710
第二次:totalTime0:2.249565 - totalTime1:2.966130
第三次:totalTime0:2.374601 - totalTime1:2.334822
第四次:totalTime0:2.824524 - totalTime1:2.441588
第五次:totalTime0:2.092460 - totalTime1:2.245025
通过上面的数据可以看到,在执行效率上并没有想象中的那么大的差别。