IOS - 自旋鎖 & atomic

本文首發於 我的博客ios

多線程中的鎖一般分爲互斥鎖和自旋鎖,這篇文章主要向你們介紹一些自旋鎖的原理以及atomic的底層實現。git

自旋鎖

⚛維基百科上對自旋鎖的解釋:程序員

自旋鎖 是計算機科學用於多線程同步的一種鎖,線程反覆檢查鎖變量是否可用。因爲線程在這一過程當中保持執行,所以是一種 忙等 (忙碌等待)。一旦獲取了自旋鎖,線程會一直持有該鎖,直至顯式釋放自旋鎖。github

獲取、釋放自旋鎖,其實是讀寫自旋鎖的存儲內存或寄存器。所以這種讀寫操做必須是原子的(atomic)。一般用 text-and-set 原子操做來實現。安全

自旋鎖的核心就是忙等,嘗試自定義一個自旋鎖以下:markdown

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方法相關,咱們找到了相關方法的實現:async

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copybool 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那塊加了判斷,若是是原子性就會進行加鎖和解鎖操做。oop

再看 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, assignNSInteger number;

- (void)atomicTest {
    dispatch_async(dispatch_get_global_queue(00), ^{
        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(00), ^{
        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 屬性和線程安全並無必然聯繫。
相關文章
相關標籤/搜索