atomic的实现机制
atomic作用:多线程下将属性设置为atomic可以保证读取数据的一致性。因为他将保证数据只能被一个线程占用,也就是说一个线程对属性进行写操作时,会使用自旋锁锁住该属性。不允许其他的线程对其进行读取操作了。
但是它有一个很大的缺点:因为它要使用自旋锁锁住该属性,因此它会消耗更多的资源,性能会很低。要比nonatomic慢20倍。
内部实现:property 的 atomic 是采用 spinlock_t (自旋锁)实现的。
// getter方法
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic)
{
// ...
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// ...
}
//setter方法
// setter
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
// ...
if (!atomic)
{
oldValue = *slot;
*slot = newValue;
}
else
{
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
// ...
}
小结
简而言之,atomic的作用只是给getter和setter加了个锁,atomic只能保证代码进入getter或者setter函数内部时是安全的,一旦出了getter和setter,多线程安全只能靠程序员自己保障了。所以atomic属性和使用property的多线程安全并没什么直接的联系。另外,atomic由于加锁也会带来一些性能损耗,所以我们在编写iOS代码的时候,一般声明property为nonatomic,在需要做多线程安全的场景,自己去额外加锁做同步。
指针Property指向的内存区域
property可以分为值类型和引用类型。值类型就是一些基本的数据类型。而引用类型就是指的各种对象。声明为指针,指向这个属性所在的内存区域。
因此当我们访问一个属性的时候,有可能访问的是指针本身,还有可能访问的是这个指针指向的内存区域。
如:
self.userName = @"nick";
这里访问的就是指针本身,对指针进行赋值。
[self.userName rangeOfString:@"peak"];
是在访问指针指向的字符串所在的内存区域。
指针本身也是占用内存的,是固定的8个字节。
而指针指向的内存区域占用的内存就无法固定了,有可能很大,也有可能很小。我们暂且定为n。
下面举例说明一下:
@property (atomic) NSArray *array;
atomic修饰的实际上是这个指针,也就是占8个字节内存的指针,因此就不可能随意使用多线程来操作这块内存的。因为这块内存是原子性的。是线程安全的。
真正不安全的是指针指向的那块内存区域,他是非原子性的,当多个线程去操作这块内存的时候,就会出现不安全的情况。
下面我们再看两个例子:
@property (atomic, strong) NSString* stringA;
//thread A
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.stringA = @"a very long string";
}
else {
self.stringA = @"string";
}
NSLog(@"Thread A: %@\n", self.stringA);
}
//thread B
for (int i = 0; i < 100000; i ++) {
if (self.stringA.length >= 10) {
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
}
NSLog(@"Thread B: %@\n", self.stringA);
}
因为atomic修饰的是指针区域,而不是内存区域,但是我们通过self.stringA.length
和substringWithRange
访问的是property的内存区域,在前一刻读length的时候self.stringA = @"a very long string";
,下一刻取substring的时候线程A已经将self.stringA = @"string";
,立即出现out of bounds的Exception,crash。主要原因就是self.stringA.length >= 10
和NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
代码之间内存区域被其他线程修改了。
同样的场景还存在对集合类操作的时候,比如:
@property (atomic, strong) NSArray* arr;
//thread A
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.arr = @[@"1", @"2", @"3"];
}
else {
self.arr = @[@"1"];
}
NSLog(@"Thread A: %@\n", self.arr);
}
//thread B
for (int i = 0; i < 100000; i ++) {
if (self.arr.count >= 2) {
NSString* str = [self.arr objectAtIndex:1];
}
NSLog(@"Thread B: %@\n", self.arr);
}
同理,即使我们在访问objectAtIndex之前做了count的判断,线程B依旧很容易crash,原因也是由于前后两行代码之间arr所指向的内存区域被其他线程修改了。
所以真正要操心的是这一类内存区域的访问,即使用atomatic声明属性也没用。
如何做到多线程安全?(加锁)
为了避免这种crash可以通过一下代码:
//thread A
[_lock lock];
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.stringA = @"a very long string";
}
else {
self.stringA = @"string";
}
NSLog(@"Thread A: %@\n", self.stringA);
}
[_lock unlock];
//thread B
[_lock lock];
if (self.stringA.length >= 10) {
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
}
[_lock unlock];
和
//thread A
[_lock lock];
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.arr = @[@"1", @"2", @"3"];
}
else {
self.arr = @[@"1"];
}
NSLog(@"Thread A: %@\n", self.arr);
}
[_lock unlock];
//thread B
[_lock lock];
if (self.arr.count >= 2) {
NSString* str = [self.arr objectAtIndex:1];
}
[_lock unlock];