0x01 什么是线程安全
当我们碰到问题时,总会去考虑这个操作在多线程下是不是线程安全的。由此我们总能考虑到一个东西:锁
通过锁我们可以实现数据的同步操作。但这就是线程安全吗?
为此笔者查阅了一些资料得到一些关键词:
线程安全的三个特性:
-
原子性
一个操作(可能包含多个子操作)要么全部执行完毕,要不一个都不执行。 -
可见性(易被忽略,原因参见分析)
当多个线程并发访问共享变量时,一个线程对共享变量的修改,其他线程能够立即看到。 -
顺序性
程序执行的顺序按照代码的先后顺序执行
我们直接理解为:
线程安全其实就是有序的执行原子操作先保证操作的原子性,然后再保证多个这样的操作能按照我们的要求顺序执行。
可见性 容易被忽略原因分析:
CPU
从主内存中读取数据的效率相对来说并不高,现在主流的计算机中都有几级缓存。
每个线程读取共享变量时,都会将该变量加载进其对应的CPU
的告诉缓存中,修改该变量后,CPU
会立即更新缓存,但并不一定会立即将其写会主内存(实际上写会主内存的时间不可预估)。
此时其他线程(尤其是不在同一个CPU
上执行的线程)访问该变量是,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。
而这一点是操作系统或者说是硬件层面的机制,所以容易被忽略
0x02 iOS atomic
线程是否安全分析
atomic
作为访问类型,影响到的事修饰属性的setter
方法和getter
方法
如下示例代码:
声明
@property (nonatomic, copy) NSString *name;
测试代码
Person *p = [Person new];
for (int i = 0; i < 10000000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
p.name = [NSString stringWithFormat:@"hello:%d",i];
});
}
// 会直接crash
(lldb) bt
* thread #5, queue = 'com.apple.root.default-qos', stop reason = EXC_BAD_ACCESS (code=1, address=0x20)
frame #0: 0x00007fff68c45678 libobjc.A.dylib`objc_release + 24
* frame #1: 0x0000000100001b20 TestThread`-[Person setName:](self=0x0000000100470830, _cmd="setName:", name=@"hello:284") at Person.h:14:39
frame #2: 0x0000000100001d62 TestThread`__main_block_invoke(.block_descriptor=0x0000000100651290) at main.m:20:7
通过上面函数调用栈查看可以发现,crash
原因定位到 [person setName:]
中调用了objc_release
。
通过下面setName
在MRC
下的写法,可以很清楚发现多线程下release
了同一个_name
对象导致crash
- (void)setName:(NSString *)name {
[_name release]; // 最终多线程同时release一个对象导致crash
_name = [name copy];
}
修改声明为atomic
再次运行代码
@property (atomic, copy) NSString *name;
运行发现不会crash了,但是我们打印最后的用户名
Person *p = [Person new];
for (int i = 0; i < 1000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
p.name = [NSString stringWithFormat:@"hello:%d",i];
});
}
NSLog(@"用户名%@",p.name);
发现每次打印的结果都不同
2020-04-27 00:04:22.147462+0800 TestThread[14203:1179501] 用户名hello:991
这里发现,我们代码的预期是希望打印出hello:999
,但是此处却打印的是个随机结果。由此可见atomic
并未保证我们线程安全的第三点顺序性。
0x03 总结
atomic
线程是不安全的,它的安全性只是体现在读写有序,但是并不能保证外部操作的顺序性。
网友评论