OC中atomic属性如何保证线程安全

原子

原子(atom)指化学反应不可再分的基本微粒,原子在化学反应中不可分割,而在程序中一般是指不可被打断或者干扰的操作。

序言

OC中的属性可以修饰成nonatomic和atomic,即原子和非原子属性。atomic属性设计的出发点是保证多线程下使用属性的安全性,由于看不到编译器对于该语法的实际处理,对于内部的实现,流行的观点是这样的:通过对属性的set/get方法加锁实现读写的互斥来保证线程安全。但是仅仅加锁就能保证我们多线程下使用属性的安全性吗?答案是否定的。

先来看看Atomic属性的加锁版本


要保证线程安全,首先需要保证读写互斥,即读的时候不能改值,改值的时候不能读值,否则,我们读到的有可能是一个被释放的对象地址。由于ARC中加入了很多编译期语法,会对于分析问题带来干扰,我们这里的测试代码都运行于MRC下。

static NSString *const objLock = @"objLock";

@interface ViewController ()
@property (retain, atomic) NSObject *obj;
@end

@implementation ViewController
- (void)setObj:(NSObject *)obj {
    @synchronized(objLock) {
        [obj retain];
        [_obj release];
        _obj = obj;
    }
}

- (NSObject *)obj {
    @synchronized(objLock) {
        return _obj;
    }
}
@end

加锁后确实能够保持读写互斥,并且读取的时刻内存一定有效。但是这种保证,并不具备任何实用性。obj2读取到self.obj之后便走出了互斥锁的作用域,而后依然可能被释放,obj2将不再可用。

NSObject *obj2 = self.obj;
[obj2 retain];
obj2...

引用计数器的短暂维持,保证数据交接过程的安全

要保证obj2不会被其他线程意外释放,还要保证self.obj被调用后,到retain执行完这段时间内,obj的引用计数器一直大于0。这种场景下可以通过retain+autorelease来短暂维持_obj对象。

@interface ViewController ()
@property (retain, atomic) NSObject *obj;
@end

@implementation ViewController
- (void)setObj:(NSObject *)obj {
    @synchronized(objLock) {
        [obj retain];
        [_obj release];
        _obj = obj;
    }
}

- (NSObject *)obj {
    @synchronized(objLock) {
        [_obj retain];
        [_obj autorelease];
        return _obj;
    }
}
@end

调用atomic属性时引用计数器的“异常”

在调用atomic属性时,我们会发现对象的引用计数器“异常”的增加,通过对上面atomic实现原理的分析,现在就很容易理解这种现象了。

@interface ViewController ()
@property (retain, atomic) NSObject *obj;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    NSObject *obj = [NSObject new];
    self.obj = obj;
    [obj release];
    // 每调用一次self.obj会发现retainCount加1,
    // 原因在于atomic类型get方法内部对_obj做了retain+autorelease来保证交接过程中对象的有效性,
    self.obj;
    [self printRetainCount:_obj];
    self.obj;
    [self printRetainCount:_obj];
    // 加入自动释放池时retainCount表现正常
    @autoreleasepool {
        self.obj;
    }
    [self printRetainCount:_obj];
    @autoreleasepool {
        self.obj;
    }
    [self printRetainCount:_obj];
    // 打印结果:
    // 2018-03-22 15:02:31.797026+0800 MRCAtomic[78797:8115885] NSObject reference count: 2
    // 2018-03-22 15:02:31.797190+0800 MRCAtomic[78797:8115885] NSObject reference count: 3
    // 2018-03-22 15:02:31.797288+0800 MRCAtomic[78797:8115885] NSObject reference count: 3
    // 2018-03-22 15:02:31.797396+0800 MRCAtomic[78797:8115885] NSObject reference count: 3
}

- (void)printRetainCount:(NSObject *)obj {
    NSLog(@"%@ reference count: %d", NSStringFromClass(obj.class), (int)[obj retainCount]);
}
@end

看看objc源码,验证下我们的推断

找到get方法的源码和上述推测一致

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}

为什么要在MRC下做分析

MRC to ARC

ARC是一种编译期语法,编译器在编译的时候,自动加入了retain和release代码,从而自动管理了对象在所有者变化过程中引用计数器的变化,正是因为编译器的介入,这些代码对于开发人员来说是不可见的,不利于问题的定量分析,同时一些细节的特性也使得ARC有别与MRC。
例如,在ARC下,返回值为对象的方法或者函数,将会在函数return前将返回值retain一次。当从这样的函数或方法接收到返回结果时,ARC会在其包含的完整表达式结尾处释放该值,但必须遵守本地值的通常优化。
这样的函数编译器会加上ns_returns_retained属性修饰,如下:

id foo(void) __attribute((ns_returns_retained));
- (id) foo __attribute((ns_returns_retained));

这种retain和release策略使得对象在持有者变换过程中不被释放,在ARC下,调用方法获取对象比直接使用成员变量更加安全,例如,特殊情况下,我们会遇到block执行代码中释放block自身的情况,这样的代码很容易产生安全隐患。

@property (copy, nonatomic) void(^block)();

__weak typeof(self) weakself = self;
self.block = ^(){
    [obj fun1];
    // 释放block
    self->_block = nil;
    [obj fun2];
};

// 某个时刻运行了block
// 属性调用block不会崩溃,等价于MRC下 Block_copy(_block); _block(); Block_release(_block);
// 实际这种情况下,block内部并不能释放自己
self.block();

// 可能会崩溃,self->_block = nil;可能会使得block释放,从而释放掉block引用的资源,obj成为野指针,运行到[obj fun2]将会崩溃。
_block();

block的本质是函数调用:block所定义的函数+函数所引用的外部变量,Block_copy(...)实际上是强引用或者拷贝block所引用的外部对象和变量。block代码则保存在代码区不需要再额外分配内存。调用的时候则是把引用的变量传入函数,并执行函数。本质上还是一个函数调用的过程,不过增加了一层外部引用资源的维护工作。

为了实现ARC编译器做了大量的工作,这里不再一一描述,更多细节请参考:
1、Clang documentaion Objective-C Automatic Reference Counting (ARC)
2、Transitioning to ARC Release Notes

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容