Atomic原子操作原理剖析
前言
绝大部分 Objective-C 程序员使用属性时,都不太关注一个特殊的修饰前缀,一般都无脑的使用其非默认缺省的状态,他就是 atomic。
@interface PropertyClass
@property (atomic, strong) NSObject *atomicObj; //缺省也是atomic
@property (nonatomic, strong) NSObject *nonatomicObj;
@end
入门教程中一般都建议使用非原子操作,因为新手大部分操作都在主线程,用不到线程安全的特性,大量使用还会降低执行效率。
那他到底怎么实现线程安全的呢?使用了哪种技术呢?
原理
属性的实现
首先我们研究一下属性包含的内容。通过查阅源码,其结构如下:
struct property_t {
const char *name; //名字
const char *attributes; //特性
};
属性的结构比较简单,包含了固定的名字和元素,可以通过 property_getName 获取属性名,property_getAttributes 获取特性。
上例中 atomicObj 的特性为 T@"NSObject",&,V_atomicObj,其中 V 代表了 strong,atomic 特性缺省没有显示,如果是 nonatomic 则显示 N。
那到底是怎么实现原子操作的呢? 通过引入runtime,我们能调试一下调用的函数栈。

可以看到在编译时就把属性特性考虑进去了,Setter 方法直接调用了 objc_setProperty 的 atomic 版本。这里不用 runtime 去动态分析特性,应该是对执行性能的考虑。
static inline void reallySetProperty(id self, SEL _cmd,
id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) {
//偏移为0说明改的是isa
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);
}
//判断原子性
if (!atomic) {
//非原子直接赋值
oldValue = *slot;
*slot = newValue;
} else {
//原子操作使用自旋锁
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
// 取isa
if (offset == 0) {
return object_getClass(self);
}
// 非原子操作直接返回
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// 原子操作自旋锁
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// 出于性能考虑,在锁之外autorelease
return objc_autoreleaseReturnValue(value);
}
什么是自旋锁呢?
锁用于解决线程争夺资源的问题,一般分为两种,自旋锁(spin)和互斥锁(mutex)。
互斥锁可以解释为线程获取锁,发现锁被占用,就向系统申请锁空闲时唤醒他并立刻休眠。
自旋锁比较简单,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。
原子操作的颗粒度最小,只限于读写,对于性能的要求很高,如果使用了互斥锁势必在切换线程上耗费大量资源。相比之下,由于读写操作耗时比较小,能够在一个时间片内完成,自旋更适合这个场景。
自旋锁的坑
但是iOS 10之后,苹果因为一个巨大的缺陷弃用了 OSSpinLock 改为新的 os_unfair_lock。
新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。
描述引用自 ibireme 大神的文章。
我的理解是,当低优先级线程获取了锁,高优先级线程访问时陷入忙等状态,由于是循环调用,所以占用了系统调度资源,导致低优先级线程迟迟不能处理资源并释放锁,导致陷入死锁。
那为什么原子操作用的还是 spinlock_t 呢?
using spinlock_t = mutex_tt<LOCKDEBUG>;
using mutex_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
os_unfair_lock mLock; //处理了优先级的互斥锁
void lock() {
lockdebug_mutex_lock(this);
os_unfair_lock_lock_with_options_inline
(&mLock, OS_UNFAIR_LOCK_DATA_SYNCHRONIZATION);
}
void unlock() {
lockdebug_mutex_unlock(this);
os_unfair_lock_unlock_inline(&mLock);
}
}
差点被苹果骗了!原来系统中自旋锁已经全部改为互斥锁实现了,只是名称一直没有更改。
为了修复优先级反转的问题,苹果也只能放弃使用自旋锁,改用优化了性能的 os_unfair_lock,实际测试两者的效率差不多。
问答
atomic的实现机制
使用atomic 修饰属性,编译器会设置默认读写方法为原子读写,并使用互斥锁添加保护。
为什么不能保证绝对的线程安全?
单独的原子操作绝对是线程安全的,但是组合一起的操作就不能保证。
- (void)competition {
self.intSource = 0;
dispatch_async(queue1, ^{
for (int i = 0; i < 10000; i++) {
self.intSource = self.intSource + 1;
}
});
dispatch_async(queue2, ^{
for (int i = 0; i < 10000; i++) {
self.intSource = self.intSource + 1;
}
});
}
最终得到的结果肯定小于20000。当获取值的时候都是原子线程安全操作,比如两个线程依序获取了当前值 0,于是分别增量后变为了 1,所以两个队列依序写入值都是 1,所以不是线程安全的。
解决的办法应该是增加颗粒度,将读写两个操作合并为一个原子操作,从而解决写入过期数据的问题。
os_unfair_lock_t unfairLock;
- (void)competition {
self.intSource = 0;
unfairLock = &(OS_UNFAIR_LOCK_INIT);
dispatch_async(queue1, ^{
for (int i = 0; i < 10000; i++) {
os_unfair_lock_lock(unfairLock);
self.intSource = self.intSource + 1;
os_unfair_lock_unlock(unfairLock);
}
});
dispatch_async(queue2, ^{
for (int i = 0; i < 10000; i++) {
os_unfair_lock_lock(unfairLock);
self.intSource = self.intSource + 1;
os_unfair_lock_unlock(unfairLock);
}
});
}
总结
通过学习属性的原子性,对系统中锁的理解又加深,包括自旋锁,互斥锁,读写锁等。
本来都以为实现是自旋锁了,还好留了个心眼多看了一层才发现最终实现还是互斥锁。这件事也给我一个小教训,查阅源码还是要刨根问底,只浮于表面的话,可能得不到想要的真相。
引用
Atomic原子操作原理剖析的更多相关文章
- java线程:Atomic(原子的)
一.何谓Atomic? Atomic一词跟原子有点关系,后者曾被人认为是最小物质的单位.计算机中的Atomic是指不能分割成若干部分的意思.如果一段代码被认为是Atomic,则表示这段代码在执行过程中 ...
- java线程:Atomic(原子)
.何谓Atomic? Atomic一词跟原子有点关系,后者曾被人认为是最小物质的单位.计算机中的Atomic是指不能分割成若干部分的意思.如果一段代码被认为是Atomic,则表示这段代码在执行过程中, ...
- 第31课 std::atomic原子变量
一. std::atomic_flag和std::atomic (一)std::atomic_flag 1. std::atomic_flag是一个bool类型的原子变量,它有两个状态set和clea ...
- atomic 原子自增工程用法案例
案例 1 : 简单用法 atomic_int id; atomic_fetch_add(&id, 1) atomic_uint id; atomic_fetch_add(&id, 1) ...
- Java原子类操作原理剖析
◆CAS的概念◆ 对于并发控制来说,使用锁是一种悲观的策略.它总是假设每次请求都会产生冲突,如果多个线程请求同一个资源,则使用锁宁可牺牲性能也要保证线程安全.而无锁则是比较乐观的看待这个问题,它会假设 ...
- CompareAndSwap原子操作原理
在翻阅AQS(AbstractQueuedSynchronizer)类的过程中,发现其进行原子操作的时候采用的是CAS.涉及的代码如下: 1: private static final Unsafe ...
- Spark- 优化后的 shuffle 操作原理剖析
在spark新版本中,引入了 consolidation 机制,也就是说提出了ShuffleGroup的概念.一个 ShuffleMapTask 将数据写入 ResultTask 数量的本地文本,这个 ...
- 一篇文章快速搞懂 Atomic(原子整数/CAS/ABA/原子引用/原子数组/LongAdder)
前言 相信大部分开发人员,或多或少都看过或写过并发编程的代码.并发关键字除了Synchronized,还有另一大分支Atomic.如果大家没听过没用过先看基础篇,如果听过用过,请滑至底部看进阶篇,深入 ...
- C++11 并发指南六( <atomic> 类型详解二 std::atomic )
C++11 并发指南六(atomic 类型详解一 atomic_flag 介绍) 一文介绍了 C++11 中最简单的原子类型 std::atomic_flag,但是 std::atomic_flag ...
随机推荐
- ubuntu将python3设为默认后再安装支持python3.x的包
简介: ubuntu默认python2.7版本,如果想要装python3.x版本,请记住python2.7版本一定不能卸载!!!但是即使我 python3.x版本安装成功,当运行python脚本时,系 ...
- centos7.2+php7.2+nginx1.12.0+mysql5.7配置
一. 源码安装php7.2 选择需要的php版本 从 php官网: http://cn2.php.net/downloads.php 选择需要的php版本,选择.tar.gz 的下载包,点击进入,选择 ...
- 传递命令行参数示例代码 (C 和 Python)
C语言 在 C 语言中, 使用 main 函数的输入参数 argc 和 argv 传入命令行参数. argc 为 int 类型, 表示传入命令行参数的个数 (argument count); argv ...
- 在 Azure 中备份 Linux 虚拟机
可以通过定期创建备份来保护数据. Azure 备份可创建恢复点,这些恢复点存储在异地冗余的恢复保管库中. 从恢复点还原时,可以还原整个 VM,或只是还原特定的文件. 本文介绍如何将单个文件还原到运行 ...
- 如何在首次启动 Linux 虚拟机时对其进行自定义
在前面的教程中,你已学习如何通过 SSH 连接到虚拟机 (VM) 并手动安装 NGINX. 若要以快速一致的方式创建 VM,通常需要某种形式的自动化. 在首次启动 VM 时实现自定义的常见方法是使用 ...
- ExpressRoute 线路和路由域
你必须订购一条 ExpressRoute 线路 ,以通过连接提供商将你的本地基础结构连接到 Azure.下图提供了你的 WAN 与 Azure 之间的连接的逻辑表示形式. ExpressRoute 线 ...
- Oracle 补丁那些事儿(PS、PSU、CPU、SPU、BP、DBBP…)
当前ORACLE数据库提供两种方式的补丁一种是主动的Proactive Patches和另一种被动的Reactive Patches,其中Reactive Patches是指过去的ONE-OFF Pa ...
- linux centos5.8装yum安装mysql
默认的yum安装mysql都是5.1版本的 想要安装5.7的可以进行配置rpm包进行, mysql5.7安装路径 下面是默认的5.1安装路径 首先我们在使用yum安装的的时候会默认使用最新安装的,最 ...
- SQL Server 常用数据类型
char: 固定长度,存储ANSI字符,不足的补英文半角空格. varchar: 可变长度,存储ANSI字符,根据数据长度自动变化. nchar: 固定长度存储Unicode字符,汉字英文 ...
- TreeMap:是基于红黑树的Map接口的实现
> TreeMap:是基于红黑树的Map接口的实现. 红黑树:平衡二叉树 取出时,可以有三种方式:前序遍历,中序遍历,后序遍历 >排序: A 自然排序 --TreeMap无参构造 Tre ...