IOS - 自旋锁和atomic

本文首发于 个人博客

多线程中的锁通常分为互斥锁和自旋锁,这篇文章主要向大家介绍一些自旋锁的原理以及atomic的底层实现。

自旋锁

⚛维基百科上对自旋锁的解释:

自旋锁 是计算机科学用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种 忙等 (忙碌等待)。一旦获取了自旋锁,线程会一直持有该锁,直至显式释放自旋锁。

获取、释放自旋锁,实际上是读写自旋锁的存储内存或寄存器。因此这种读写操作必须是原子的(atomic)。通常用 text-and-set 原子操作来实现。

自旋锁的核心就是忙等,尝试自定义一个自旋锁如下:

struct spinlock {
    int flag;
};

@implementation TPSpinLock {
    spinlock _lock;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        _lock = spinlock{0};
    }
    return self;
}

- (void)lock {
    while (test_and_set(&_lock.flag, 1)) {
        // wait
    }
}

- (void)unlock {
    _lock.flag = 0;
}


int test_and_set(int *old_ptr, int _new) {
    int old = *old_ptr;
    *old_ptr = _new;
    return old;
}

@end

如上述代码,我们自定义了test_and_set方法,当线程1进行lock操作的时候会传入flag = 0test_and_set方法返回0的同时并将flag = 1,这个时候线程2 执行lock的时候一直返回1,那么就一直执行while(1)处于等待状态,直到线程1执行unlockflag = 0 这个时候就打破while循环,线程2就能继续执行并加锁。

atomic

说起自旋锁,无不联想到属性的原子操作,即 atomic

  • atomic 底层是如何实现的?
  • atomic 绝对安全吗?

带着这些问题我们对 atomic 进行探讨,我们来到 objc源码 处进行查看,atomic 既然是修饰property的,那么必然会跟propertysetget方法相关,我们找到了相关方法的实现:

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);
    }
    // 原子操作判断
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

set 方法atomic那块加了判断,如果是原子性就会进行加锁和解锁操作。

再看 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);
}

很明显,也是对原子操作进行加锁处理。

我们注意到所有的源码中针对加锁的地方都是定义为spinlock,也就是自旋锁,所以通常被人问到我们atomic底层是什么的时候,我们都回答 自旋锁 ,结合YY大神的不再安全的OSSpinLock 一文,可以看出Apple已经弃用OSSpinLock了,内部确如下述代码那样是用os_unfair_lock 来实现的,探其底层执行lockunlock的其实是mutex_t,也就是互斥锁

// property的set方法
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
// atomic中用到的锁
using spinlock_t = mutex_tt<LOCKDEBUG>;
// mutex_tt 的结构
class mutex_tt : nocopy_t {
    os_unfair_lock mLock;
 public:
    constexpr mutex_tt() : mLock(OS_UNFAIR_LOCK_INIT) {
        lockdebug_remember_mutex(this);
    }

    constexpr mutex_tt(const fork_unsafe_lock_t unsafe) : mLock(OS_UNFAIR_LOCK_INIT) { }

    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);
    }
// 上述lock方法的实现
void 
lockdebug_mutex_lock(mutex_t *lock)
{
    auto& locks = ownedLocks();

    if (hasLock(locks, lock, MUTEX)) {
        _objc_fatal("deadlock: relocking mutex");
    }
    setLock(locks, lock, MUTEX);
}

所以说 atomic 的本质并不是自旋锁,至少当前不是,我查询了 objc 之前的源码发现老版本的 atomic 的实现,确实不一样:

typedef uintptr_t spin_lock_t;
OBJC_EXTERN void _spin_lock(spin_lock_t *lockp);
OBJC_EXTERN int  _spin_lock_try(spin_lock_t *lockp);
OBJC_EXTERN void _spin_unlock(spin_lock_t *lockp);

由此可知:

atomic 原子操作只是对settergetter 方法进行加锁

那么第二个问题来了:atomic绝对安全吗?我们接着分析,首先看下面的代码,最终的 number 会是多少?20000?

@property (atomic, assign) NSInteger number;

- (void)atomicTest {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10000; i ++) {
            self.number = self.number + 1;
            NSLog(@"A-self.number is %ld",self.number);
        }
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10000; i ++) {
            self.number = self.number + 1;
            NSLog(@"B-self.number is %ld",self.number);
        }
    });
}

打印结果:

NO 并不是 20000,这是为啥呢?我们的 numberatomic 进行加锁了啊,为什么还会出现线程安全问题。其实答案上文已经有了,只是需要我们仔细去品,atomic 只是针对 settergetter 方法进行加锁,上述代码有两个异步线程同时执行,如果某个时间 A线程 执行到getter方法,之后 cpu 立即切换到 线程B 去执行他的get方法那么这个时候他们进行 +1 的处理并执行setter方法,那么两个线程的 number 就会是一样的结果,这样我们的 +1就会出现线程安全问题,就会导致我们的数字出现偏差,那么我们找一找打印数字里是否有重复的:

功夫不负有心人,我们果然找到了重复的,那么基于我们 20000 的循环次数少个百八十的太正常不过了。

总结

  • 自旋锁 不同于互斥锁 如果访问的资源被占用,它会处于 忙等 状态。自旋锁由于一直处于忙等状态所以他在线程锁被释放的时候会立即获取而不用唤醒,所以其执行效率是很高的,尤其是在多核的cpu上运行效率很高,但是其忙等的状态会消耗cpu的性能,所以其性能比互斥锁要低很多。
  • atomic 的底层实现,老版本是自旋锁,新版本是互斥锁
  • atomic 并不是绝对线程安全,它能保证代码进入 gettersetter 方法的时候是安全的,但是并不能保证多线程的访问情况下是安全的,一旦出了 gettersetter 方法,其线程安全就要由程序员自己来把握,所以 atomic 属性和线程安全并没有必然联系。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容

  • 1. 什么情况下会有线程隐患? 我们在使用多线程技术带来的便利的同时,也需要考虑下多线程所带来的隐患。比如,我们可...
    沉江小鱼阅读 814评论 0 11
  • 转载:谈 iOS 的锁 又到了春天挪坑的季节,想起多次被问及到锁的概念,决定好好总结一番。 翻看目前关于 iOS ...
    小鲲鹏阅读 520评论 0 0
  • 目录 1、为什么要线程安全 2、自旋锁和互斥锁 3、锁的类型1、OSSpinLock2、os_unfair_loc...
    SunshineBrother阅读 1,163评论 0 20
  • Q:为什么出现多线程? A:为了实现同时干多件事的需求(并发),同时进行着下载和页面UI刷新。对于处理器,为每个线...
    幸福相依阅读 1,576评论 0 2
  • 前言 对于iOS中各种锁的学习总结,供日后查阅 引子 日常开发中,@property (nonatomic, st...
    Tr2e阅读 884评论 1 1