iOS底層學習 - 多線程之中的鎖🔐

經過以前篇章的學習,咱們對整個GCD從使用到原理,都有了必定的理解。這篇主要講解一下iOS開發中的鎖是什麼狀況html

系列文章傳送門:ios

iOS底層學習 - 多線程之基礎原理篇面試

iOS底層學習 - 多線程之GCD初探編程

iOS底層學習 - 多線程之GCD隊列原理篇swift

iOS底層學習 - 多線程之GCD應用篇數組

iOS底層學習 - 多線程之GCD底層原理篇緩存

基礎小概念

什麼是鎖

鎖 -- 是保證線程安全常見的同步工具。鎖是一種非強制的機制,每個線程在訪問數據或者資源前,要先獲取(Acquire) 鎖,並在訪問結束以後釋放(Release)鎖。若是鎖已經被佔用,其它試圖獲取鎖的線程會等待,直到鎖從新可用。安全

鎖的做用

前面說到了,鎖是用來保護線程安全的工具。markdown

能夠試想一下,多線程編程時,沒有鎖的狀況 -- 也就是線程不安全。多線程

當多個線程同時對一塊內存發生讀和寫的操做,可能出現意料以外的結果:

程序執行的順序會被打亂,可能形成提早釋放一個變量,計算結果錯誤等狀況。

因此咱們須要將線程不安全的代碼 「鎖」 起來。保證一段代碼或者多段代碼操做的原子性,保證多個線程對同一個數據的訪問 同步 (Synchronization)。

鎖的分類

鎖的分類方式,能夠根據鎖的狀態,鎖的特性等進行不一樣的分類,不少鎖之間其實並非並列的關係,而是一種鎖下的不一樣實現。能夠看這篇文章JAVA中鎖的分類

互斥鎖與自旋鎖

互斥鎖:是⼀種⽤於多線程編程中,防⽌兩條線程同時對同⼀公共資源(⽐ 如全局變量)進⾏讀寫的機制。當獲取鎖操做失敗時,線程會進入睡眠,等待鎖釋放時被喚醒。 互斥鎖又分爲遞歸鎖和非遞歸鎖。

  • 遞歸鎖:可重入鎖,同一個線程在鎖釋放前可再次獲取鎖,便可以遞歸調用。
  • 非遞歸鎖:不可重入,必須等鎖釋放後才能再次獲取鎖。

⾃旋鎖:線程反覆檢查鎖變量是否可⽤。因爲線程在這⼀過程當中保持執⾏, 所以是⼀種忙等待。⼀旦獲取了⾃旋鎖,線程會⼀直保持該鎖,直⾄顯式釋 放⾃旋鎖。 ⾃旋鎖避免了進程上下⽂的調度開銷,所以對於線程只會阻塞很 短期的場合是有效的。

互斥鎖與自旋鎖區別:

其實就是線程的區別,互斥鎖在線程獲取鎖但沒有獲取到時,線程會進入休眠狀態,等鎖被釋放時,線程會被喚醒,而自旋鎖的線程則會一直處於等待狀態,忙等待,不會進入休眠。

自旋鎖

1. OSSpinLock

相信你們都拜讀過這片文章->再也不安全的 OSSpinLock。總結來講,自旋鎖之因此不安全,是由於因爲自旋鎖獲取鎖時,線程會一直處於忙等待狀態,形成了任務的優先級反轉。

OSSpinLock 忙等的機制,就可能形成高優先級一直 running ,佔用 CPU 時間片。而低優先級任務沒法搶佔時間片,變成遲遲完不成,不釋放鎖的狀況。

2. atomic

在面試中,咱們常常遇到關於atomic相關的問題,總結來講主要是兩個方面,一個是atomic的底層原理是怎樣的,另外一個是使用atomic是否就能保證線程安全。

關於底層原理,咱們仍是來看源碼進行探索。經過源碼,咱們能夠發現,在方法的setget方法中,會有是不是atomic的判斷,若是不是的話,則直接進行賦值,若是是的話,會加一個spinlock_t的鎖,這個鎖保證了對屬性讀寫的安全。

void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    // ...

    if (!atomic) {
        // 不是 atomic 修飾
        oldValue = *slot;
        *slot = newValue;
    } else {
        // 若是是 atomic 修飾,加一把同步鎖,保證 setter 的安全
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
}

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    // ...
    
    // 非原子屬性,直接返回值
    if (!atomic) return *slot;
    // 原子屬性,加同步鎖,保證 getter 的安全
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
}

複製代碼

既然atomic是保證setget方法安全的,那是否是就說明其線程安全呢?其實並非的,這隻能保證該屬性在單一線程上是安全的,若是是有不少的線程對該屬性進行同時的操做,那麼就不能保證其數據安全了.好比下面的代碼,經過結果咱們能夠看到,並無起到加鎖的效果。

//Thread A
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 100; i ++) {
            self.num = self.num + 1;
            NSLog(@"Thread A:%ld\n",self.num);
        }
    });
    
    //Thread B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 100; i ++) {
            NSLog(@"Thread B:%ld\n",self.num);
        }
    });
    
-------------------------------------------------------------
Thread A:1
Thread B:1
Thread B:2
Thread A:2
    
複製代碼

3. 讀寫鎖

讀寫鎖實際是⼀種特殊的⾃旋鎖,它把對共享資源的訪問者劃分紅讀者和寫者,讀者只對共享資源進⾏讀訪問,寫者則須要對共享資源進⾏寫操做。這種鎖相對於⾃旋鎖⽽⾔,能提⾼併發性,由於在多處理器系統中,它容許同時有多個讀者來訪問共享資源,最⼤可能的讀者數爲實際的邏輯CPU數。

  • 寫者是排他性的,⼀個讀寫鎖同時只能有⼀個寫者或多個讀者(與CPU數相關),但不能同時既有讀者⼜有寫者。在讀寫鎖保持期間也是搶佔失效的。

  • 若是讀寫鎖當前沒有讀者,也沒有寫者,那麼寫者能夠⽴刻得到讀寫鎖,不然它必須⾃旋在那⾥,直到沒有任何寫者或讀者。若是讀寫鎖沒有寫者,那麼讀者能夠⽴即得到該讀寫鎖,不然讀者必須⾃旋在那⾥,直到寫者釋放該讀寫鎖。

具體用法以下,不過在平常開發中較少使用

// 須要導入頭文件
#include <pthread.h>

pthread_rwlock_t lock;
// 初始化鎖
pthread_rwlock_init(&lock, NULL);
// 讀-加鎖
pthread_rwlock_rdlock(&lock);
// 讀-嘗試加鎖
pthread_rwlock_tryrdlock(&lock);
// 寫-加鎖
pthread_rwlock_wrlock(&lock);
// 寫-嘗試加鎖
pthread_rwlock_trywrlock(&lock);
// 解鎖
pthread_rwlock_unlock(&lock);
// 銷燬
pthread_rwlock_destroy(&lock);
複製代碼

咱們可使用併發隊列+dispatch_barrier_async來實現一個相似的讀寫鎖

########### .h文件
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface WY_RWLock : NSObject
// 讀數據
- (id)wy_objectForKey:(NSString *)key;
// 寫數據
- (void)wy_setObject:(id)obj forKey:(NSString *)key;
@end

NS_ASSUME_NONNULL_END

########### .m文件
#import "WY_RWLock.h"

@interface WY_RWLock ()
// 定義一個併發隊列:
@property (nonatomic, strong) dispatch_queue_t concurrent_queue;
// 多個線程須要數據訪問
@property (nonatomic, strong) NSMutableDictionary *dataCenterDic;
@end

@implementation WY_RWLock

- (id)init{
    self = [super init];
    if (self){
        // 建立一個併發隊列:
        self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
        // 建立數據字典:
        self.dataCenterDic = [NSMutableDictionary dictionary];
    }
    return self;
}

#pragma mark - 讀數據
- (id)wy_objectForKey:(NSString *)key{
    __block id obj;
    // 同步讀取指定數據:
    dispatch_sync(self.concurrent_queue, ^{
        obj = [self.dataCenterDic objectForKey:key];
    });
    return obj;
}

#pragma mark - 寫數據
- (void)wy_setObject:(id)obj forKey:(NSString *)key{
    // 異步柵欄調用設置數據:
    dispatch_barrier_async(self.concurrent_queue, ^{
        [self.dataCenterDic setObject:obj forKey:key];
    });
}

@end

複製代碼

互斥鎖

互斥鎖爲何安全

由於互斥鎖出現優先級反轉後,高優先級的任務不會忙等。由於處於等待狀態的高優先級任務,沒有佔用時間片,因此低優先級任務通常都能進行下去,從而釋放掉鎖。

互斥鎖性能

1. @synchronized

@synchronized的使用很是簡單,代碼以下,傳入一個想要加鎖的對象,在其中執行加鎖的相關邏輯便可。

@synchronized (obj) {}
複製代碼

那麼其底層邏輯是如何實現的呢,咱們能夠看一下@synchronized的源碼,經過打斷點,查看其彙編源碼,發現@synchronized就是實現了objc_sync_enterobjc_sync_exit兩個方法,也就是說是經過這兩個方法來實現加鎖和解鎖操做的。經過符號斷點,咱們能夠知道其代碼在objc源碼中。

首先注意enterexit中都首先對obj是否爲nil作了判斷,若是obj爲空時,則不會進行加鎖和解鎖的相關操做。因此在使用時必定要注意傳入的值會不會被析構,形成傳入值爲空的狀況,從而加鎖失敗

好比在線程異步同時操做同一個對象時,由於遞歸鎖會不停的alloc/release,這時候某一個對象會多是nil,從而致使加鎖失敗

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing// 若是obj爲空,則不進行加鎖操做
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}

-----------------------------------------------------------------------------------------------------------------------

int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        ✅// 若是obj爲空,則不進行解鎖操做
        // @synchronized(nil) does nothing
    }
	

    return result;
}

複製代碼

在具體的實現邏輯中,咱們能夠看到經過id2data方法,對obj進行了捕獲和釋放的操做,並生成了一個SyncData類型的對象。咱們發現SyncData是一個結構體,並且有一個SyncData類型的nextData變量,指向下個數據,因此咱們能夠知道SyncData是一個鏈表結構中的一個元素。因此這是一個遞歸鎖。

  • nextData指的是鏈表中下一個元素
  • object指的是傳入須要加解鎖的對象
  • threadCount就表示當前的線程數量
  • mutex即對象所關聯的鎖
typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;
複製代碼

瞭解了SyncData結構後,咱們繼續來查看源碼,因爲源碼比較長,因此咱們分模塊倆講解。

1.1 準備SyncData

咱們能夠看到會會經過LOCK_FOR_OBJLIST_FOR_OBJ取出object所對應的lockplistp

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    ...
}

複製代碼

既然咱們在任何地方均可以直接經過調用方法來使用,那麼說明底層必然維護着一套內部的存儲。經過代碼咱們也能夠看出,系統在底層維護了一個哈希表,裏面存儲了SyncList結構的數據,而SyncList是一個結構體,包含一個SyncData的頭結點和一個spinlock_t鎖對象

-----------------------------------------------------------------------------------------------------------------------
struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
-----------------------------------------------------------------------------------------------------------------------
// Use multiple parallel lists to decrease contention among unrelated objects.
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data

static StripedMap<SyncList> sDataLists;
複製代碼

1.2 快速檢查線程緩存

此步操做會經過tls封裝的相關pthead操做線程的相關增刪改查方法,獲取到單個線程中緩存的SyncData數據,並進行快速查詢和緩存

static SyncData* id2data(id object, enum usage why)
{
    ...
    #if SUPPORT_DIRECT_THREAD_KEYS
    // Check per-thread single-entry fast cache for matching object// 檢查每線程單項快速緩存中是否有匹配的對象
    bool fastCacheOccupied = NO;
        ✅// 經過tls相關封裝的pthead方法獲取是否有再底層存儲的SyncData
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        fastCacheOccupied = YES;
        ✅// 若是獲取到的數據和傳入數據相同
        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            switch(why) {
            case ACQUIRE: {
                // 若是是 entry,則對 lockCount 加 1,並經過 tls 保存
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:
                // 若是是 exit,則對 lockCount 減 1,並經過 tls 保存
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    // remove from fast cache
                    // 若是 lockCount 爲 0,則從高速緩存中刪除
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif
    ...
}
複製代碼

1.3 檢查有鎖線程中的緩存

這步操做是檢查全部線程中的緩存

static SyncData* id2data(id object, enum usage why)
{
    ...
    // Check per-thread cache of already-owned locks for matching object
    // 檢查已擁有鎖的每一個線程高速緩存中是否有匹配的對象
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        unsigned int i;
        for (i = 0; i < cache->used; i++) {
            SyncCacheItem *item = &cache->list[i];
            if (item->data->object != object) continue;

            // Found a match.
            result = item->data;
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
                
            switch(why) {
            case ACQUIRE:
                item->lockCount++;
                break;
            case RELEASE:
                item->lockCount--;
                if (item->lockCount == 0) {
                    // remove from per-thread cache
                    cache->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
    ...
}
複製代碼

1.4 全局哈希表查找

若是上述兩步中,單個線程和已經鎖住的線程中的緩存數據都沒有找到的話,那麼就會來到此步,回來系統保存的哈希表中SyncList結果中,進行鏈式查找。

static SyncData* id2data(id object, enum usage why)
{
    ...
    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }
    ...
}
複製代碼

1.5 生成新數據並寫入緩存

static SyncData* id2data(id object, enum usage why)
{
    ...
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;
    
 done:
    lockp->unlock();
    if (result) {
        // Only new ACQUIRE should get here.
        // All RELEASE and CHECK and recursive ACQUIRE are 
        // handled by the per-thread caches above.// 只有建立的 SyncData 才能進入這裏。// 全部的釋放、檢查和遞歸獲取都是由上面的線程緩存處理
        if (why == RELEASE) {
            // Probably some thread is incorrectly exiting 
            // while the object is held by another thread.
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) {
            // Save in fast thread cache// 存入快速線程緩存
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
#endif
        {
            // Save in thread cache// 存入線程緩存
            if (!cache) cache = fetch_cache(YES);
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

    return result;
}
複製代碼

至此一個@synchronized的相關操做已經執行完成。總結來講就是底層保存了一個哈希表,其中存儲了SyncData結構的一個鏈表,經過線程緩存等操做,來進行增刪改查,歷來實現加解鎖。可是操做結構複雜,步驟多,致使性能較滴,並且須要注意傳入的obj不能爲空,不然沒法進行鎖操做。

2. dispatch_semaphore

相關信號量的底層原理,再上一章節已經講過,能夠直接查看☞iOS底層學習 - 多線程之GCD底層原理篇

3. NSLock

NSLock的使用也很是的簡單,只須要再須要進行加鎖邏輯的先後,加上[_lock lock][_lock unlock]兩行代碼,就能夠實現加鎖的邏輯。

在尋找源碼中,咱們發現NSLock源碼在CoreFundation框架中,沒法進行查看,因此咱們看Swift版本的CoreFundation實現,來類比NSLock實現,應該也是差很少的。經過源碼咱們能夠發現

  • NSLock就是對pthread_mutex互斥鎖的一種上層封裝。
  • 是一種互斥鎖,但不是遞歸鎖
open class NSLock: NSObject, NSLocking {
    internal var mutex = _MutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
    private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
    private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif

    public override init() {
        pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
        pthread_cond_init(timeoutCond, nil)
        pthread_mutex_init(timeoutMutex, nil)
#endif
    }
    
    open func lock() {
        pthread_mutex_lock(mutex)
    }
    open func unlock() {
        pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
        // Wakeup any threads waiting in lock(before:)
        pthread_mutex_lock(timeoutMutex)
        pthread_cond_broadcast(timeoutCond)
        pthread_mutex_unlock(timeoutMutex)
#endif
    }

複製代碼

既然NSLock不是遞歸鎖,那麼他就存在着一個坑點:當咱們對同一個線程,加鎖兩次的話,就會形成一直阻塞,就好比下面的代碼,多線程調用時,會形成lock屢次,從而沒法向下進行。這個時候可使用遞歸鎖來解決。

NSLock *testlock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            [testlock lock];
            if (value > 0) {
                NSLog(@"current value = %d",value);
                // 異步遞歸調用
                testMethod(value - 1);
            }
            [testlock unlock];
        };
        testMethod(10);
    });
複製代碼

3. NSRecursiveLock

將上面例子中的NSLock換成NSRecursiveLock就是遞歸鎖的使用了,和NSLock是相似的,而且可以解決NSLock在多線程中屢次加鎖的問題。

首先咱們仍是來看一下源碼實現,發現NSRecursiveLock也是對pthread_mutex的封裝,可是初始化的時候添加了PTHREAD_MUTEX_RECURSIVE遞歸相關的操做。

open class NSRecursiveLock: NSObject, NSLocking {
    internal var mutex = _RecursiveMutexPointer.allocate(capacity: 1)
    private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
    private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
    
    withUnsafeMutablePointer(to: &attrib) { attrs in
            pthread_mutexattr_init(attrs)
            pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
            pthread_mutex_init(mutex, attrs)
        }
        
    pthread_cond_init(timeoutCond, nil)
    pthread_mutex_init(timeoutMutex, nil)

    public override init() {
        super.init()
        var attrib = pthread_mutexattr_t()
        pthread_cond_init(timeoutCond, nil)
        pthread_mutex_init(timeoutMutex, nil)
    }
    
    deinit {
        pthread_mutex_destroy(mutex)
        mutex.deinitialize(count: 1)
        mutex.deallocate()
        deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
    }
    
    open func lock() {
        pthread_mutex_lock(mutex)
    }
    
    open func unlock() {
        pthread_mutex_unlock(mutex)
        // Wakeup any threads waiting in lock(before:)
        pthread_mutex_lock(timeoutMutex)
        pthread_cond_broadcast(timeoutCond)
        pthread_mutex_unlock(timeoutMutex)
    }
    
    open func `try`() -> Bool {
        return pthread_mutex_trylock(mutex) == 0
    }
    
    open func lock(before limit: Date) -> Bool {
        if pthread_mutex_trylock(mutex) == 0 {
            return true
        }
        return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
    }
    open var name: String?
}
複製代碼

咱們都知道,使用遞歸的時候,最主要的是要有一個出口,不然很是容易造成死鎖。好比剛纔的代碼,若是進行for循環建立多線程時。這時候就是形成死鎖崩潰。

由於這個時候for循環形成多線程的屢次建立,開闢了多條線程,可是NSRecursiveLock對象只有一個,線程之間同一個鎖的對象狀態是不能共享的,因此形成了線程1進行lock後,未執行到unlock時,線程2就進行了lock,因此形成了線程 1 等線程 2 解鎖,線程 2 等線程 1 解鎖的死鎖情況。

那麼這種狀況下,使用哪一種方案比較好呢?

這個時候使用@synchronized能夠完美解決問題,由於@synchronized鎖的是同一個對象,下次線程來進行鎖操做時,會先從緩存中進行查找,不會進行屢次鎖,因此是安全的。

NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
for (int i = 0; i < 100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            [recursiveLock lock];
            if (value > 0) {
                NSLog(@"current value = %d",value);
                testMethod(value - 1);
            }
            [recursiveLock unlock];
        };
        testMethod(10);
    });
}
複製代碼
經常使用鎖總結:當只是普通線程安全的時候,使用 NSLock就能夠解決,而須要保證遞歸調用線程安全的時候,使用 NSRecursiveLock,而又須要循環,外界的線程也會形成影響的時候,爲了解決死鎖的問題,咱們可使用@synchronized來解決
複製代碼

4. NSCondition

NSCondition是一個條件鎖。

在線程間的同步中,有這樣一種狀況: 線程 A 須要等條件 C 成立,才能繼續往下執行.如今這個條件不成立,線程 A 就阻塞等待. 而線程 B 在執行過程當中,使條件 C 成立了,就喚醒線程 A 繼續執行。這個時候,咱們可使用條件鎖來完成相關邏輯。

條件鎖的底層實現其實就是一個互斥鎖和條件變量的封裝,因爲未開源,咱們仍是先看Swift源碼。

  • NSCondition是對mutexcond的一種封裝。cond就是用於訪問和操做特定類型數據的指針
  • wait操做在沒有超時時,會阻塞線程,使其進入休眠狀態,須要在lock狀態下使用
  • signal操做是喚醒一個正在休眠等待的線程,須要在lock狀態下使用
  • broadcast喚醒全部正在等待的線程,須要在lock狀態下使用
open class NSCondition: NSObject, NSLocking {
        internal var mutex = _MutexPointer.allocate(capacity: 1)
    // 用於訪問和操做特定類型數據的指針
    internal var cond = _ConditionVariablePointer.allocate(capacity: 1)

    public override init() {
        pthread_mutex_init(mutex, nil)
        pthread_cond_init(cond, nil)
    }
    
    open func lock() {
        pthread_mutex_lock(mutex)
    }
    open func unlock() {
        pthread_mutex_unlock(mutex)
    }
    open func wait() {
        pthread_cond_wait(cond, mutex)
    }
    open func wait(until limit: Date) -> Bool {
        // 超時
        guard var timeout = timeSpecFrom(date: limit) else {
            return false
        }
        // 沒有超時
        return pthread_cond_timedwait(cond, mutex, &timeout) == 0
    }
    open func signal() {
        pthread_cond_signal(cond)
    }
    open func broadcast() {
        pthread_cond_broadcast(cond) // wait signal
    }

}
複製代碼

對於條件鎖,咱們常常用來解決的就是生產者-消費者模式的相關問題。好比數組中的元素,只有在大於0的狀況下,才能夠進行刪除操做,這種狀況下,能夠考慮使用條件鎖。

_condition = [[NSCondition alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [self producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [self consumer];
});

- (void)producer{
    [_condition lock];
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生產一個 現有 count %zd",self.ticketCount);
    [_condition signal];
    [_condition unlock];

}

- (void)consumer{
    
    // 線程安全
    [_condition lock];
    ✅// 使用while由於NSCondition能夠給每一個線程分別加鎖,但加鎖後不影響其餘線程進入臨界區。// 因此 NSCondition使用 wait並加鎖後,並不能真正保證線程的安全。// 當一個signal操做發出時,若是有兩個線程都在作消費者操做,那同時都會消耗掉資源,因而繞過了檢查。

    while (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        // 保證正常流程
        [_condition wait];
    }
    
    //注意消費行爲,要在等待條件判斷以後
    self.ticketCount -= 1;
    NSLog(@"消費一個 還剩 count %zd ",self.ticketCount);
    [_condition unlock];
}

複製代碼

5. NSConditionLock

NSConditionLock。咱們能夠經過Swift源碼查看可得

  • NSConditionLockNSCondition加線程數的封裝,繼承NSLocking協議,也有lockunlock等方法
  • 實現了相似dispatch_semaphore的效果
open class NSConditionLock : NSObject, NSLocking {
    internal var _cond = NSCondition()
    internal var _value: Int
    internal var _thread: _swift_CFThreadRef?
    
    public convenience override init() {
        self.init(condition: 0)
    }
    
    public init(condition: Int) {
        _value = condition
    }

    open func lock() {
        let _ = lock(before: Date.distantFuture)
    }

    open func unlock() {
        _cond.lock()
        _thread = nil
        _cond.broadcast()
        _cond.unlock()
    }
    
    open var condition: Int {
        return _value
    }

    open func lock(whenCondition condition: Int) {
        let _ = lock(whenCondition: condition, before: Date.distantFuture)
    }

    open func `try`() -> Bool {
        return lock(before: Date.distantPast)
    }
    
    open func tryLock(whenCondition condition: Int) -> Bool {
        return lock(whenCondition: condition, before: Date.distantPast)
    }

    open func unlock(withCondition condition: Int) {
        _cond.lock()
        _thread = nil
        _value = condition
        _cond.broadcast()
        _cond.unlock()
    }

    open func lock(before limit: Date) -> Bool {
        _cond.lock()
        while _thread != nil {
            if !_cond.wait(until: limit) {
                _cond.unlock()
                return false
            }
        }
        _thread = pthread_self()
        _cond.unlock()
        return true
    }
    
    open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
        // 使用 NSCondition 加鎖
        _cond.lock()
        while _thread != nil || _value != condition {
            if !_cond.wait(until: limit) {
                _cond.unlock()
                return false
            }
        }
        _thread = pthread_self()
        _cond.unlock()
        return true
    }
    
    open var name: String?
}

複製代碼

具體的用法能夠參考下面的代碼

// 初始化 NSConditionLock,並設置 condition 的值爲 2
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    // 須要等到 condition 爲 1 的時候執行下面的代碼
    [conditionLock lockWhenCondition:1];
    NSLog(@"線程 1");
    [conditionLock unlockWithCondition:0];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    // 由於 condition 爲 2,因此執行下面的代碼
    [conditionLock lockWhenCondition:2];
    NSLog(@"線程 2");
    // 解鎖,並將 condition 設置爲 1
    [conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 由於沒有條件限制,因此能夠直接執行下面的代碼
    [conditionLock lock];
    NSLog(@"線程 3");
    [conditionLock unlock];
});
-----------------------------------------------------------------------------------------------------------------------
// 打印結果
線程 3
線程 2
線程 1

複製代碼

6. os_unfair_lock

因爲OSSpinLock自旋鎖的bug,在iOS10以後OSSpinLock被廢棄,內部封裝了os_unfair_lock,而os_unfair_lock在加鎖時會處於休眠狀態,而不是自旋鎖的忙等狀態。

總結

  • OSSpinLock之因此不在安全,是由於自旋鎖會在線程等待時處於忙等狀態,會形成任務優先級翻轉,卻是沒法執行,目前用os_unfair_lock來替代,是一個互斥鎖,互斥鎖不會處於忙等,不佔用時間片。
  • atomic底層實現原理就是對getset方法進行加鎖,可是不能保證多條線程調用或者不適用getset的線程安全,且性能消耗巨大
  • 讀寫鎖實際是⼀種特殊的⾃旋鎖,只容許一個寫者寫入,可是能夠有多個讀者。可使用併發隊列+dispatch_barrier_async的方法,來實現一個相似的讀寫鎖
  • @synchronized要注意傳入的對象不能爲nil,不然沒法加鎖。底層邏輯是維護了一個全局的哈希表用來存儲對象和鎖,會按照緩存線程->全部線程->全局哈希表的方式進行增刪改查
  • NSLock是對pthread_mutex的封裝,可是沒有遞歸邏輯。對同一個線程屢次lock會形成阻塞。NSRecursiveLock是在NSLock的基礎上添加了遞歸邏輯,當只有一個遞歸鎖對象,多線程進行鎖操做時,會形成死鎖,可用@synchronized解決
  • NSConditionNSConditionLock是條件鎖,當知足某一個條件時,才能進行操做,適用於生產者消費者模式,和信號量dispatch_semaphore相似

參考資料

iOS 的鎖

iOS 鎖的底層探索筆記

相關文章
相關標籤/搜索