59 (OC)* atomic是否绝对安全

场景:如今项目中有这样一个场景,在一个自定义类型的Property在一个线程中改变的同时也要同时在另一个线程中使用它,使我不得不将Property定义成atomic,但是由此发现atomic并不会保证线程安全,由此我深入查询了解下atomic 与 nonatomic,发现自己之前并不了解atomic 与 nonatomic。

正文:

首先,我们先要弄懂一个问题:什么是原子性?

原子操作是不可分割的操作,在原子操作执行完毕之前,其不会被任何其它任务或事件中断。

被标注atomic会保证这种对Property的频繁操作的原子性,可以避免由两个操作对同一个Property同时进行操作而造成的错误。

atomic与nonatomic内部实现的区别只是atomic对象setter和getter方法会加一个锁,而nonatomic并没有,代码如下:

@property (nonatomic) NSObject *nonatomicObj;
@property (atomic) NSObject *atomicObj;


- (void)setNonatomicObj:(NSObject *)nonatomicObj{
if (_nonatomicObj != nonatomicObj) {
[_nonatomicObj release];
_nonatomicObj = [nonatomicObj retain];
}
}

- (NSObject *)nonatomicObj{
return _nonatomicObj;
}

- (void)setAtomicObj:(NSObject *)atomicObj{
@synchronized(self) {
if (_atomicObj != atomicObj) {
[_atomicObj release];
_atomicObj = [atomicObj retain];
}
}
}

- (NSObject *)atomicObj{
@synchronized(self) {
return _atomicObj;
}
}
其次,原子性是不是代表线程安全?

我们先看一下苹果开发文档

 

苹果开发文档已经明确指出:Atomic不能保证对象多线程的安全。所以Atomic 不能保证对象多线程的安全。它只是能保证你访问的时候给你返回一个完好无损的Value而已。举个例子:

如果线程 A 调了 getter,与此同时线程 B 、线程 C 都调了 setter——那最后线程 A get 到的值,有3种可能:可能是 B、C set 之前原始的值,也可能是 B set 的值,也可能是 C set 的值。同时,最终这个属性的值,可能是 B set 的值,也有可能是 C set 的值。所以atomic可并不能保证对象的线程安全。

atomic和nonatomic的对比:

1、atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。

2、atomic:系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。getter 还是能得到一个完好无损的对象(可以保证数据的完整性),但这个对象在多线程的情况下是不能确定的,比如上面的例子。

也就是说:如果有多个线程同时调用setter的话,不会出现某一个线程执行完setter全部语句之前,另一个线程开始执行setter情况,相当于函数头尾加了锁一样,每次只能有一个线程调用对象的setter方法,所以可以保证数据的完整性。

atomic所说的线程安全只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的。

3、nonatomic:就没有这个保证了,nonatomic返回你的对象可能就不是完整的value。因此,在多线程的环境下原子操作是非常必要的,否则有可能会引起错误的结果。但仅仅使用atomic并不会使得对象线程安全,我们还要为对象线程添加lock来确保线程的安全。

4、nonatomic的速度要比atomic的快。

5、atomic与nonatomic的本质区别其实也就是在setter方法上的操作不同

 

总结:

所以atomic的作用只是保证了Property的原子性,在多线程环境下同时操作它时,无论何时取值,都可以取到一个完整值,但是并不能保证线程安全,具体例子参照上文。所以如果想要保证线程安全,单单把用atomic来标注是完全不够的,还需要通过上锁或其他方式老保证线程安全!

 

 atomic 是如何在有效范围内安全的 

atomic 实际上就是原子操作,这个概念其实并不新鲜,早在linux系统下编程本身也是有这个东西的,所谓原子,就是不可再化分,已经是最小的操作单位(所谓操作指的是对内存的读写)网上很多地方都在讨论oc下的atomic 不安全,不能保证数据的并发性,实际上有一点误导了大家,认为atomic 本身是不安全的
实际上,并非atomic 不安全,而是网上一些说法有问题 

 

所谓一个数据的线程安全,简单点来说就是这块数据即使有多个线程同时读写,也不会出现数据的错乱,内存的最后状态总是可以预见的,如果这块内存的数据被一个多线程读写之后,出现的结果是不可预见的,那么就可以说这块内存是“线程不安全的”

 

atomic 实际上相当于一个引用计数器,这个大家很熟悉,如果被标记了atomic,那么被标记了的内存本身就有了一个引用计数器,第一个占用这块内存的线程,会给这个计数器+1,在这个线程操作这块内存期间,其他线程在访问这个内存的时候,如果发现“引用计数器”不为0,则阻塞,实际上阻塞并不等于休眠,他是基于cpu轮询片,休眠除非被叫醒,否则无法继续执行,阻塞则不同,每个cpu 轮询片到这个线程的时候都会尝试继续往下执行,可见 阻塞相对于休眠来讲,阻塞是主动的,休眠是被动的,如果引用计数器为0,轮询片到来,则先给这块内存的引用计数器+1,然后再去操作,atomic 实现操作序列化的具体过程大概就是如此,说来很容易理解,但是为什么还会有歧义?

 

那所有的指针类型都会有这个问题。

以oc 下的 NSArray * 为例子,如果一个多线程操作这个数据,会有两个层级的并发问题

1、指针本身

2、指针所指向的内存 

上面已经说了,指针本身也是占用内存的,并且一定是8个字节,第二部分,指针所指向的内存,这个占多少字节就不一定了,有可能非常大,有可能也就1个字节

所以我们考虑NSArray * array 这个数据array 多线程操作的时候,必须分成两部分来描述,一个是&array这个指针本身,另一个则是它所指向的内存 array

大家注意下 &array 和 array 的区别 ,其实不用纠结,你就想象现在有两块内存,一块是8字节,一块n字节,8字节里面放的值,就是n字节内存的首地址

ok 现在联系上atomic,如果用@property(atomic)NSArray *array 修饰之后,会有什么影响?网上说的很多,不再赘述,我只想从内存的角度来解释这个过程

首先第一点,你要记住,@property(atomic)NSArray *array 其实修饰的是这个指针,也就是这个8字节内存,跟第二部分数据n字节没有任何关系,被atomic 修饰之后,你不可能随意去多线程操作这个8字节,但是对8字节里面所指向的n字节没有任何限制!这就是所有网络上所说的 atomic 不安全的真想 !

我们来看一下,这能怪atomic? 本身你修饰的是一个指针,并且atomic 已经完美的履行了它的指责,你现在不可能对这个8字节进行无序的多线程操作,这就够了呀!atomic没有任何鸟问题。有问题的是人,你本身并未对n字节做任何的限制,所以把问题怪罪到atomic 上真的是很不合理。

另外我们回忆一下网络的说法,说atomic 只对 get 和 set 方法起作用,这个说法很容易理解

我们知道,这个8字节里面存储的数据,是n字节数据的头地址,如果更改8字节数据的内容,那么最后通过这个指针访问到的数据就会完全不一样,这个可以理解吧?8字节相当于楼管,里面的数据相当于整栋楼的钥匙,给你不同的钥匙,你是不是就进的是不同的房间? 

通过atomic 我们可以保证这个“指针”被有序的访问,也仅仅只能保证到这。

自旋锁已被证明不安全,同步锁简单,性能差,nslock 性能略好,dispatch_semphone 性能最好 

现在我们有一个8字节的指针,假如我们做一个初始化 NSArray *array = [[NSArray alloc] init] 这个操作。实际上这个操作有两个意思

1:给8字节赋值

2:开辟了一块n字节的内存区

我们只说这8自己的地址复制,如果没有atomic 修饰,并且假设现在有两个线程正在操作这个指针,一个就是上面的初始化线程,另一个线程就是读这个8自己的指针

首先,假如8字节内部存放的是0x1122334455667788 ok 8字节需要写入这个指,但与此同时,很不巧,另一个读线程现在要读这个8字节里面的值

假如 这个8字节只写了一半的时候 另一个线程来读,那它读到的可能是 0x1122334400000000 OK 实际上,等他读完之后,写线程仍然还未完成

这时候,[[NSArray alloc] init] 的头地址正确的应该是0x1122334455667788 ,而读线程读到的是0x1122334400000000 这时候会出现什么情况?

最好的情况,无非就是个野指针,因为谁也不知道这块地址是否有效或者是否有什么重要的数据,也指针会导致啥不多说了

最坏的情况,这个野地址指向的是重要的一段数据。。。后果可想而知。

所以 atomic 的意义就在于此,在0x1122334455667788 写完之前,读线程是无法读取的,同样的道理,在读线程正在读的过程中,写线程是无法改变8字节的 。atomic 能避免这8字节的值因为多线程的原因被意外破坏,仅此而已。

假如现在有atomic 修饰,假如现在有两个线程正在操作这个指针,根据上面的结论,他俩“先后”正确的获取到了内存地址,也就说,他俩都先后、正确的找到了8字节内容所指向的n字节内容,虽然找到这n个字节内容的顺序有先后,但是不影响这两个线程同时去操作这n个字节的数据。

这样问题又来了,两个线程同时去操作n字节内容,如果两个线程都是读线程,一般不会有问题,但是假如至少有一个是写线程,那问题又来了,还是一个读写同步的问题,因此 atomic 虽然规范了 找到这n字节内容的先后顺序,但是它不能规范对着n个字节内容的读写。这就是atomic 的局限性。

如果是指针变量,需要加锁,如果是基本变量,不用考虑,不需要加锁 。

posted on 2019-08-24 11:20  风zk  阅读(705)  评论(0编辑  收藏  举报

导航