Objective-C 原子属性

2018-02-09 12:42:15来源:http://liuduo.me/2018/02/08/objective-c-atomic/作者:刘铎.Me人点击

分享


Objective-C 在声明一个属性的时候,想必大家都是不用经过大脑思考就会写@property (nonatomic, ...


我们都知道属性可以是 nonatomic 也可以使 atomic 的,但是好像几乎所有属性在声明的时候 nonatomic,atomic 的属性几乎没出现过。atomic 修饰符仿佛已被大家遗忘。



实际上,如果声明属性时既不写 atomic 也不写 nonatomic,那么这个属性默认是 atomic 的


atomic 的作用和工作原理

从字面上来看 nonatomic 是非原子的,atomic 是原子的。


atomic 的作用为:


atomic 修饰的属性的写操作是一个原子操作。


什么是原子操作?

原子操作就是指不会被线程调度机制打断的操作。这个操作是一个整体,CPU 一旦开始执行它,就会一直到执行结束,在这期间 CPU 不会转而去执行其它线程的操作。


可以用代码来模拟一下 atomic 的工作原理:


#import "ViewController.h"
@interface ViewController ()
@property (atomic, assign) NSInteger count;
@end
@implementation ViewController
@synthesize count = _count;
- (void)setCount:(NSInteger)count {
@synchronized (self) {
_count = count;
}
}
- (NSInteger)count {
return _count;
}
...


看上面代码,ViewController 有个 count 属性,我们重写了它的 Setter 和 Getter 方法,在 Setter 方法中,通过@synchronized (self) {}
为复制操作_count = count
加了一把锁,使得赋值这个操作同一时间只能有一个线程执行,保证了写属性值的时候的线程安全。



上面代码实现了和 atomic 相同的功能,但是底层的工作方式还是有区别的。我们常常用@synchronized
来加锁,这种锁是互斥锁
。而 atomic 修饰的属性自带了一把自旋锁


互斥锁和自旋锁的区别:

锁名
作用

互斥锁
当某个资源被先进入的线程上了锁以后,其它后面进入的线程会进入休眠状态
。当锁释放后,进入休眠状态的线程变为唤醒状态。

自旋锁
当某个资源被先进入的线程上了锁以后,其它后进入的线程会开启一个循环
,不断检查锁有没有释放,当锁释放后,退出循环开始访问资源,整个过程中后进入的线程一直保持运行状态


大多数情况下,atomic 并不能保证线程安全

既然 atomic 能简单的让一个属性的写操作变成线程安全的,为什么几乎不用它?


下面看一个简单的例子:


#import "ViewController.h"
@interface ViewController ()
@property (atomic, assign) NSInteger count;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.count = 0;
NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething) object:nil];
[threadA start];
NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething) object:nil];
[threadB start];
}
- (void)doSomething {
for (NSInteger i = 0; i < 10; i++) {
[NSThread sleepForTimeInterval:1.0];
self.count++;
NSLog(@"self.count = %@ %@", @(self.count), [NSThread currentThread]);
}
}
@end


上面代码中,把属性 count 声明为 atomic 的。在 viewDidLoad 中创建了两个线程 threadA 和 threadB,都去执行 doSomething 方法。在 doSomething 方法中,去给 self.count 的值通过每次循环 +1 增加 10 次,然后打印 self.count 的值。为了让异常情况出现的概率提高,加入一句[NSThread sleepForTimeInterval:1.0];


运行上面的代码,会发现打印的结果中,最后一条 self.count 的值往往是小于 20 的,在中间的某些打印日志中,会发现有些数字被重复打印的两次。


...
2018-02-07 23:05:08.718744+0800 AtomicDemo[53388:2777211] self.count = 13 <NSThread: 0x600000265f40>{number = 4, name = (null)}
2018-02-07 23:05:08.718791+0800 AtomicDemo[53388:2777210] self.count = 14 <NSThread: 0x600000265f00>{number = 3, name = (null)}
2018-02-07 23:05:09.719374+0800 AtomicDemo[53388:2777210] self.count = 15 <NSThread: 0x600000265f00>{number = 3, name = (null)}
2018-02-07 23:05:09.719374+0800 AtomicDemo[53388:2777211] self.count = 15 <NSThread: 0x600000265f40>{number = 4, name = (null)}
2018-02-07 23:05:10.719673+0800 AtomicDemo[53388:2777211] self.count = 17 <NSThread: 0x600000265f40>{number = 4, name = (null)}
2018-02-07 23:05:10.719673+0800 AtomicDemo[53388:2777210] self.count = 16 <NSThread: 0x600000265f00>{number = 3, name = (null)}
...

上面的结果中 15 出现了两次,这说明在使用 atomic 的情况下,还是出现了资源竞争。


那么原因在哪里呢?


我们看这句代码:


self.count++;

这句代码做了两件事,先读取 self.count 的值,然后把读取到的值 + 1 后赋值给 self.count。


由于 atomic 仅仅能保证写是线程安全的,而不是保证 读 -> +1 -> 写,这个整体是线程安全的。


当两个线程都执行到读取完 self.count 的值后,再去写,就会写成一样的值。


所以大部分情况下,为了保证线程安全,还是要自己加锁,可以根据需要来保证某块代码整体的线程安全。


线程安全的代码:


- (void)doSomething {
for (NSInteger i = 0; i < 10; i++) {
[NSThread sleepForTimeInterval:1.0];
@synchronized (self) {
self.count++;
}
NSLog(@"self.count = %@ %@", @(self.count), [NSThread currentThread]);
}
}

因为 atomic 在大部分情况下都无法保证线程安全,并且 atomic 的属性因为增加了原子性而降低了执行效率,因此实际开发中几乎不会出现 atomic 的身影。


nonatomic 对比 atomic

最后简单对比一下 nonatomic 和 atomic



修饰符
优势
劣势

nonatomic
执行效率高,性能好
不是线程安全的

atomic
线程安全,但是仅能保证写操作的线程安全
大幅降低执行效率

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台