iOS 数组线程安全

概述

为什么会有数组的线程安全问题?
对于可变的集合(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

通过上面的数据可以看到,在执行效率上并没有想象中的那么大的差别。

深入分析待续....
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容