整理自论坛一位大神的回复,非原创
首先我表明自己的立场先,实际上atomic 是安全的。而且是绝对安全的
atomic 实际上就是原子操作,这个概念其实并不新鲜,早在linux系统下编程本身也是有这个东西的,所谓原子,就是不可再化分,已经是最小的操作单位(所谓操作指的是对内存的读写)网上很多地方都在讨论oc下的atomic 不安全,不能保证数据的并发性,实际上有一点误导了大家,认为atomic 本身是不安全的
实际上,并非atomic 不安全,而是网上一些说法有问题,下面就来剖析一下 atomic 是如何在有效范围内安全的
所谓一个数据的线程安全,简单点来说就是这块数据即使有多个线程同时读写,也不会出现数据的错乱,内存的最后状态总是可以预见的,如果这块内存的数据被一个多线程读写之后,出现的结果是不可预见的,那么就可以说这块内存是“线程不安全的”
其实这个状态很容易理解,同一个箱子,有的人在里面放球,有的人从里面拿,如果没有一个有规则的顺序,都乱哄哄的一起进行,那么最后箱子里几个球只能靠猜了,所以atomic 就是解决“线程操作同一块内存顺序”的一个方案
atomic 实际上相当于一个引用计数器,这个大家很熟悉,如果被标记了atomic,那么被标记了的内存本身就有了一个引用计数器,第一个占用这块内存的线程,会给这个计数器+1,在这个线程操作这块内存期间,其他线程在访问这个内存的时候,如果发现“引用计数器”不为0,则阻塞,实际上阻塞并不等于休眠,他是基于cpu轮询片,休眠除非被叫醒,否则无法继续执行,阻塞则不同,每个cpu 轮询片到这个线程的时候都会尝试继续往下执行,可见 阻塞相对于休眠来讲,阻塞是主动的,休眠是被动的,如果引用计数器为0,轮询片到来,则先给这块内存的引用计数器+1,然后再去操作,atomic 实现操作序列化的具体过程大概就是如此,说来很容易理解,但是为什么还会有歧义?
首先我们从最基本的数据类型说起,char int long dobule 比如是这四种,如果在64位系统下,他们分别占1、4、8、8 个字节。想象一下,1个字节也是内存,8个字节也是内存,只要是内存,就有可能产生所谓的资源竞争,也就是多线程并发的问题。据我所知,char int 是绝对线程安全的,也就是说 系统对 char int 类型数据的操作,要么不操作,要么绝对会把这些字节全部操作完,不会并发的问题,也就不会产生所谓所线程访问的问题。也就说,即使是一个并发的多线程,对一个char 或者int 字节进行操作,无论怎么操作,无论情况如何极端,都不会导致数据的错乱,可以这么说:char int 是绝对线程安全的 。再来说 long double 这两个并没有char int 那般的特殊待遇,实际上,这两个分别占了8个字节,对一个long型数据的读写,操作系统有可能分两部分进行,一部分是高位,一部分是低位,所以两个线程同时操作long数据,有可能导致数据不同步,但是据我写的demo测试,这种情况很难出现,因为一个long型的数据最多也就8个字节,对着8个字节都是微妙甚至纳秒级别,一个cpu的轮询片的周期,极大情况下都会比这个时间长,也就说,即使long 不是线程安全的,但是由于本身字节非常少,读写速度极快,快到比cpu轮询的时间片都块的情况下,实际上即使不线程安全,两个线程也不会同时读写这块内存。
但凡事都有例外,demo 不能出现不代表他就是线程安全的,只能说绝大部分情况下,多线程操作一个long 数据问题是不大的。
继续说double,double 类型的数据同样占8个字节,按道理来说,他读写的速度应当和long 一样快。实际上,也是这样的。但是。一个线程读写内存仅仅只是一个方面,cpu 需要对数据进行计算,这个计算的中间结果一般都会放到寄存器或者cpu 高速缓存,double 数据计算的复杂度远非long型所能比,因此double 数据类型相比long 型数据更容易出现并发的问题
但是我实际测试的结果,也没有出现什么问题
说到这里其实应该总结一下窍门了,什么样的数据会存在多线程的问题?什么样的数据不会呢?
可以想象一下,如果一个数据占的内存特别大,读写这块数据需要的时间也就越长,如果这个时间长度远远超过线程调度的轮询片,那么就有极大可能出现并发问题。单单这样说其实还是不能归纳那些数据才会出现并发问题
你就记住,小于等于4个字节的基本类型数据,比如char short int 等等等等,都是线程安全的,只要大于这个规定,都不是线程安全的。。。
ok 我们继续讨论一种特殊的数据类型,指针类型
我们知道,在64位的操作系统下,所有类型的指针,包括void * 都是占用8个字节的。我们上面已经说了,超过4个字节的基本类型数据都会有线程并发的问题。
那所有的指针类型都会有这个问题。
以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 我们可以保证这个“指针”被有序的访问,也仅仅只能保证到这。
现在我们有一个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 的局限性
这就跟推塔一个道理,你站在塔内,别人进来肯定被打,你跑出去塔的攻击范围,被人搞,然后抱怨塔不起作用,这貌似真的很搞笑
------内存内容安全问题
@synchronized
如果代码执行时间是1ms 加上@sy 之后是2ms 这就是严重影响性能
但是对你主观的影响,1ms 和2ms 貌似没什么感觉
目前最推荐实用的就是 dispatch_semphone 但是这个用起来代码量比较大, @syxxxxx 这个性能虽然不咋地,但是就多2行代码,实际上最好的设计是尽量避免出现同步问题,因为一旦出现同步,必然要加锁,加锁之后的执行顺序其实是穿行的,又因为有开锁解锁的过程,因此其效率比串行执行效率更差,效率跟逻辑是一个矛盾体,只能找到平衡点,不可能找到绝对完美的解决方案,自旋锁(比如OSSpinLockLock)已被证明不安全,同步锁简单,性能差,nslock 性能略好,dispatch_semphone 性能最好。
如果是指针变量,需要加锁,如果是基本变量,不用考虑,不需要加锁
----cup轮询片
假如现在只有一个cpu 核心,但是我现在想线程并发,怎么办? 假如有1、2 两个线程想并发
cpu 就会执行1下1 然后再执行1下2 然后再执行1下1.。。。。。。。。。。 如此不停的切换,这就是cpu 轮询