iOS-atomic修饰符原理剖析讲解 (你将会了解到什么是优先级翻转、自旋锁、互斥锁)

前言

这里面你将会了解到什么是\color{red}{优先级翻转、自旋锁、互斥锁}

绝大部分 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,我们能调试一下调用的函数栈。

image.png

可以看到在编译时就把属性特性考虑进去了,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)。

互斥锁可以解释为线程获取锁,发现锁被占用,就向系统申请锁空闲时唤醒他并立刻休眠。互斥锁加锁的时候,等待锁的线程处于休眠状态,不会占用CPU的资源

自旋锁比较简单,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。自旋锁加锁的时候,等待锁的线程处于忙等状态,并且占用着CPU的资源。

原子操作的颗粒度最小,只限于读写,对于性能的要求很高,如果使用了互斥锁势必在切换线程上耗费大量资源。相比之下,由于读写操作耗时比较小,能够在一个时间片内完成,自旋更适合这个场景。

自旋锁的坑

但是iOS 10之后,苹果因为一个巨大的缺陷弃用了 OSSpinLock 改为新的 os_unfair_lock。

新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。

描述引用自 ibireme 大神的文章。

\color{red}{优先级翻转的问题}

新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。

具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。导致陷入死锁。

这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。iOS10以后,苹果给出了新的api

那为什么原子操作用的还是 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,实际测试两者的效率差不多。

os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持

从底层调用看,等待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);
    }
  });
}

总结

通过学习属性的原子性,对系统中锁的理解又加深,包括自旋锁,互斥锁,读写锁等。

本来都以为实现是自旋锁了,还好留了个心眼多看了一层才发现最终实现还是互斥锁。这件事也给我一个小教训,查阅源码还是要刨根问底,只浮于表面的话,可能得不到想要的真相。

引用

可以编译的runtime库

不再安全的 OSSpinLock

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

推荐阅读更多精彩内容