线程安全三要素

2020-04-27  本文已影响0人  哦小小树

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

通过下面setNameMRC下的写法,可以很清楚发现多线程下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线程是不安全的,它的安全性只是体现在读写有序,但是并不能保证外部操作的顺序性。

上一篇 下一篇

猜你喜欢

热点阅读