0%

OC atomic修饰符

源码版本:objc4-781,objc-accessors.mm文件中。

原子操作是指操作是不可分割的,要么发生,要么不发生。事物只会处于原始状态和成功状态两种中的一种,不会处于一种半完成状态。

OC 中的atomic修饰符

源码实现:

getter 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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); //+1
slotlock.unlock();

// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value); //注册到自动释放池中,延迟-1
}

对于none atomic属性,getter方法只是简单的返回对象地址。
对于atomic属性,getter方法会加锁进行retain操作,并返回一个注册到自动释放池里的对象地址。这样就可以防止getter的对象不会因为其他线程的setter操作导致销毁。

setter 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}

id oldValue;
id *slot = (id*) ((char*)self + offset);

if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue); //retain新值
}

if (!atomic) {
oldValue = *slot; //获取旧值
*slot = newValue; //赋予新值
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot; //
*slot = newValue;
slotlock.unlock();
}

objc_release(oldValue); //release 旧值
}

void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}

void objc_setProperty_nonatomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, false, false, false);
}


void objc_setProperty_atomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, true, true, false);
}

void objc_setProperty_nonatomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, false, true, false);
}

上述锁从何而来:从objc-accessors.mm中定义的全局PropertyLocks哈希表中获取。

1
2
3
StripedMap<spinlock_t> PropertyLocks;
StripedMap<spinlock_t> StructLocks;
StripedMap<spinlock_t> CppObjectLocks;

可以看到使用atomic修饰的属性,编译器在合成getter/setter方法时,相比于nonatomic的属性会有额外的处理:

  1. 加锁(自旋锁),保证原子性。其他线程需要等待。
  2. getter方法获取到对象后会先retain再注册到自动释放池里最后返回给调用者,确保调用者获取到的对象生命周期不受后续setter影响。

atomic保证了赋值/取值的整个过程的完整性,并且不受其他线程的干扰。比如线程A在setter时,线程B如果想setter那么就需要等待,线程C想getter也需要等待。

再来看一下使用nonatomic在多线程环境下,下面的场景会出现什么问题:

1、A,B线程同时调用setter方法会怎样。

1
2
3
4
5
6
7
8
9
10
11
12
if (!atomic) {
1 oldValue = *slot; //获取旧值
2 *slot = newValue; //赋予新值
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot; //
*slot = newValue;
slotlock.unlock();
}

3 objc_release(oldValue); //release 旧值

A,B同时执行1,因此获得了同一个旧值对象,然后各自执行2因此属性的值是不确定的(这里倒没有什么致命关系),最后执行3,于是一个对象被释放了两次,这有可能会导致野指针崩溃。而atomic有加锁保护所以不会有问题。

2、线程A调用getter方法获得对象地址,在对象使用期间,线程B调用setter方法,会怎样?

getter方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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); //+1
slotlock.unlock();

// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}

对于nonatomic,getter方法获取到旧值后只是简单返回。上述情况nonatomic可能会因为线程B调用setter方法释放旧值而造成线程A潜在的野指针访问崩溃风险。而atomic会先retain再注册到自动释放池里最后返回给调用者,因此不会存在这种风险。

iOS12.5.4 iPhone6plus

例1:

例2:

这些都是野指针崩溃,访问了已经销毁的对象。

因此如果属性存在多线程访问的情况,应该使用atomic并使用存取器访问实例变量,如果有需要还应该配合锁使用。

注意:atomic无法保证对象自身的线程安全。

官方文档对atomic无法保证对象的线程安全举的例子:

考虑XYZPerson,拥有firstName和lastName两个atomic属性。如果一个线程A正在修改这两个属性,与此同时另一个线程B正在读取这两个属性,那么线程B得到的firstName和lastName的值将会发生数据不匹配的情况。这虽然不会导致崩溃问题,但结果是不符合预期的。

eg:

初始时, firstName = @”李”; lastName = @”四”;

线程A想修改为:firstName = @”张”; lastName = @”三”;

与此同时线程B在读取这两个属性,那么线程B有可能得到:

张四,李三。

要想解决的话就应该把firstName = @"张"; lastName = @"三";这一段代码也加锁。

另外一个例子:经典的i++问题。

i++对应的汇编:

1
2
3
0x100368d83 <+19>: movl   -0x14(%rbp), %eax
0x100368d86 <+22>: addl $0x1, %eax
0x100368d89 <+25>: movl %eax, -0x14(%rbp)

可以看到,完成一次i++总共分为三步:

  • 从内存中取出i的值存放到寄存器上
  • 对寄存器的值+1
  • 将寄存器中的值存放回i的内存

由于需要三条指令,因此在多线程中是不安全的。比如线程A在执行第二步时,线程B刚好在执行第一步,此时线程 B 获取到的将是旧的值,最终i的值将只加了一次。

再来看一下OC 中self.i++;的汇编,i 为 atomic 属性。

源码:

1
2
3
- (void)test_atomic_xiushi {
self.i++;
}

对应的汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
multithreadDemo`-[XQLockViewController test_atomic_xiushi]:
0x101c90d60 <+0>: pushq %rbp
0x101c90d61 <+1>: movq %rsp, %rbp
0x101c90d64 <+4>: subq $0x20, %rsp
0x101c90d68 <+8>: movq %rdi, -0x8(%rbp)
0x101c90d6c <+12>: movq %rsi, -0x10(%rbp)
0x101c90d70 <+16>: movq -0x8(%rbp), %rax
0x101c90d74 <+20>: movq 0x155f5(%rip), %rsi ; "'i'"
0x101c90d7b <+27>: movq %rax, %rcx
0x101c90d7e <+30>: movq %rcx, %rdi
0x101c90d81 <+33>: movq %rax, -0x18(%rbp)
0x101c90d85 <+37>: callq *0xe2b5(%rip) ; (void *)0x00000001025ec940: objc_msgSend #调用objc_msgSend(self, "i")
0x101c90d8b <+43>: addq $0x1, %rax #将寄存器中的值加 1
0x101c90d91 <+49>: movq 0x156c8(%rip), %rsi ; "setI:"
0x101c90d98 <+56>: movq -0x18(%rbp), %rcx
0x101c90d9c <+60>: movq %rcx, %rdi
0x101c90d9f <+63>: movq %rax, %rdx
-> 0x101c90da2 <+66>: callq *0xe298(%rip) ; (void *)0x00000001025ec940: objc_msgSend #调用objc_msgSend(self, "setI:", %rdx)
0x101c90da8 <+72>: addq $0x20, %rsp
0x101c90dac <+76>: popq %rbp
0x101c90dad <+77>: retq

步骤:

1
2
3
4
5
1.调用objc_msgSend(self, "i");  

2.执行addq $0x1, %rax #将寄存器中的值加 1

3.调用objc_msgSend(self, "setI:", %rdx);

跟上面的普通 i++其实是一样的,只不过这里 1,3 执行到 getter/setter 时都会加锁,但问题还是出在第 2 步。当线程 A 正在执行第2步还没来得及执行第 3 步将值写入内存时,线程 B 是可以执行第 1 步的,此时线程 B 将获取到旧的值。于是最终只加了一次。要想线程安全必须将 1,2,3都包在锁内 。

即:

1
2
3
加锁
self.i++;
解锁

因此即使是atomic的属性也是不能保证上述代码是线程安全的。

总结

atomic只是保证了设值/取值的过程是完整的,不受其他线程的干扰。它无法保证其他使用逻辑是线程安全的

比如上面的i++操作。

场景: 使用atomic修饰属性,如果有A、B和C三个线程。其中A和B线程同时对一个属性进行赋值操作,C线程进行取值操作,那么可以保证C线程一定可以取到一个完整的值,但是这个值的内容可能是A线程赋的值,也可能是B线程赋的值,也可能是原始值,虽然取得了完整的值,但是这个值不一定是程序员想要的,所以说atomic并不是线程安全的,它只是保证了属性的setter和getter方法内部是线程安全的。如果你想要真正保证线程安全,那么需要在赋值操作的前后进行加锁和解锁操作,还有注意使用同一把锁。

有没有仅使用atomic就能够满足要求的场景?我想象不到,如果你只需要属性存取线程安全,或许仅使用atomic就可以了。比如上面使用atomic就可以避免多线程下的野指针问题。因此atomic的使用场景有限。

参考

多线程-线程安全

Properties Are Atomic by Default 官方文档

OC 基础之atomic和nonatomic关键字

觉得文章有帮助可以打赏一下哦!