Objective-C 在声明一个属性的时候,想必大家都是不用经过大脑思考就会写 @property (nonatomic, ...
。
我们都知道属性可以是 nonatomic 也可以使 atomic 的,但是好像几乎所有属性在声明的时候 nonatomic,atomic 的属性几乎没出现过。atomic 修饰符仿佛已被大家遗忘。
实际上,如果声明属性时既不写 atomic 也不写 nonatomic,那么这个属性默认是 atomic 的。
atomic 的作用和工作原理
从字面上来看 nonatomic 是非原子的,atomic 是原子的。
atomic 的作用为:
atomic 修饰的属性的写操作是一个原子操作。
什么是原子操作?
原子操作就是指不会被线程调度机制打断的操作。这个操作是一个整体,CPU 一旦开始执行它,就会一直到执行结束,在这期间 CPU 不会转而去执行其它线程的操作。
可以用代码来模拟一下 atomic 的工作原理:
1 |
|
看上面代码,ViewController 有个 count 属性,我们重写了它的 Setter 和 Getter 方法,在 Setter 方法中,通过 @synchronized (self) {}
为复制操作 _count = count
加了一把锁,使得赋值这个操作同一时间只能有一个线程执行,保证了写属性值的时候的线程安全。
上面代码实现了和 atomic 相同的功能,但是底层的工作方式还是有区别的。我们常常用 @synchronized
来加锁,这种锁是互斥锁。而 atomic 修饰的属性自带了一把自旋锁。
互斥锁和自旋锁的区别:
锁名 | 作用 |
---|---|
互斥锁 | 当某个资源被先进入的线程上了锁以后,其它后面进入的线程会进入休眠状态。当锁释放后,进入休眠状态的线程变为唤醒状态。 |
自旋锁 | 当某个资源被先进入的线程上了锁以后,其它后进入的线程会开启一个循环,不断检查锁有没有释放,当锁释放后,退出循环开始访问资源,整个过程中后进入的线程一直保持运行状态。 |
大多数情况下,atomic 并不能保证线程安全
既然 atomic 能简单的让一个属性的写操作变成线程安全的,为什么几乎不用它?
下面看一个简单的例子:
1 |
|
上面代码中,把属性 count 声明为 atomic 的。在 viewDidLoad 中创建了两个线程 threadA 和 threadB,都去执行 doSomething 方法。在 doSomething 方法中,去给 self.count 的值通过每次循环 +1 增加 10 次,然后打印 self.count 的值。为了让异常情况出现的概率提高,加入一句 [NSThread sleepForTimeInterval:1.0];
。
运行上面的代码,会发现打印的结果中,最后一条 self.count 的值往往是小于 20 的,在中间的某些打印日志中,会发现有些数字被重复打印的两次。
1 | ... |
上面的结果中 15 出现了两次,这说明在使用 atomic 的情况下,还是出现了资源竞争。
那么原因在哪里呢?
我们看这句代码:
1 | self.count++; |
这句代码做了两件事,先读取 self.count 的值,然后把读取到的值 + 1 后赋值给 self.count。
由于 atomic 仅仅能保证写是线程安全的,而不是保证 读 -> +1 -> 写,这个整体是线程安全的。
当两个线程都执行到读取完 self.count 的值后,再去写,就会写成一样的值。
所以大部分情况下,为了保证线程安全,还是要自己加锁,可以根据需要来保证某块代码整体的线程安全。
线程安全的代码:
1 | - (void)doSomething { |
因为 atomic 在大部分情况下都无法保证线程安全,并且 atomic 的属性因为增加了原子性而降低了执行效率,因此实际开发中几乎不会出现 atomic 的身影。
nonatomic 对比 atomic
最后简单对比一下 nonatomic 和 atomic
修饰符 | 优势 | 劣势 |
---|---|---|
nonatomic | 执行效率高,性能好 | 不是线程安全的 |
atomic | 线程安全,但是仅能保证写操作的线程安全 | 大幅降低执行效率 |