iOS進階之路 (十七)多線程 - 鎖的底層原理和使用

如今操做系統基本都是多任務操做系統,即同時有大量可調度實體在運行。在多任務操做系統中,同時運行的多個任務可能:ios

  • 都須要訪問/使用同一種資源
  • 多個任務之間有依賴關係,某個任務的運行依賴於另外一個任務。

同步:是指散步在不一樣任務之間的若干程序片斷,它們的運行必須嚴格按照規定的某種前後次序。最基本的場景就是:多個線程在運行過程當中協同步調,按照預約的前後次序運行。好比A任務的運行依賴於B任務產生的數據程序員

互斥:是指散步在不一樣任務之間的若干程序片斷,當某個任務運行其中一個程序片斷時,其餘任務就不能運行它們之間的任一程序片斷,直到該任務運行完畢。最基本的場景就是:一個公共資源同一時刻只能被一個進程使用面試

咱們可使用鎖來解決多線程的同步和互斥問題,基本的鎖包括三類:互斥鎖 自旋鎖 讀寫鎖, 其餘的好比條件鎖 遞歸鎖 信號量都是上層的封裝和實現。objective-c

一. 互斥鎖

互斥鎖是一種用於多線程編程中,防止兩條線程同時對同一公共資源(比 如全局變量)進行讀寫的機制。該目的經過將代碼切片成一個一個的臨界區而達成。算法

互斥鎖能夠分爲 遞歸鎖(recursive mutex)非遞歸鎖(non-recursive mutex)。兩者惟一的區別是,同一個線程能夠屢次獲取同一個遞歸鎖,不會產生死鎖。而若是一個線程屢次獲取同一個非遞歸鎖,則會產生死鎖。macos

  1. 互斥鎖的特色:
  • 原子性:若是一個線程鎖定了一個互斥量,沒有其餘線程在同一時間能夠成功鎖定這個互斥量;
  • 惟一性:若是一個線程鎖定了一個互斥量,在它解除鎖定以前,沒有其餘線程能夠鎖定這個互斥量;
  • 非繁忙等待:若是一個線程鎖定了一個互斥量,第二個線程又試圖去鎖定這個互斥量,則第二個線程將被掛起(不佔用任何cpu資源),直到第一個線程解除對這個互斥量的鎖定爲止,第二個線程則被喚醒並繼續執行,同時鎖定這個互斥量。
  1. 互斥鎖的工做流程:
  • 在訪問共享資源後臨界區域前,對互斥鎖進行加鎖;
  • 對互斥鎖進行加鎖後,任何其餘試圖再次對互斥鎖加鎖的線程將會被阻塞,直到鎖被釋放。
  • 在訪問完成後釋放互斥鎖導上的鎖;
  1. 經常使用的互斥鎖
  • @synchronized
  • NSLock
  • NSRecursive

1.1 pthread_mutex

#include <pthread.h>
#include <time.h>
// 初始化一個互斥鎖。
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

// 對互斥鎖上鎖,若互斥鎖已經上鎖,則調用者一直阻塞,直到互斥鎖解鎖後再上鎖。
int pthread_mutex_lock(pthread_mutex_t *mutex);

// 調用該函數時,若互斥鎖未加鎖,則上鎖,返回 0;若互斥鎖已加鎖,則函數直接返回失敗,即 EBUSY。
int pthread_mutex_trylock(pthread_mutex_t *mutex);

// 當線程試圖獲取一個已加鎖的互斥量時,pthread_mutex_timedlock 互斥量
// 容許綁定線程阻塞時間。即非阻塞加鎖互斥量。
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

// 對指定的互斥鎖解鎖。
int pthread_mutex_unlock(pthread_mutex_t *mutex);

// 銷燬指定的一個互斥鎖。互斥鎖在使用完畢後,必需要對互斥鎖進行銷燬,以釋放資源。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
複製代碼

對於 pthread_mutex 來講,比較重要的是鎖的類型,摘自百度百科:編程

  • PTHREAD_MUTEX_NORMAL:不提供死鎖檢測。嘗試從新鎖定互斥鎖會致使死鎖。若是某個線程嘗試解除鎖定的互斥鎖不是由該線程鎖定或未鎖定,則將產生不肯定的行爲。
  • PTHREAD_MUTEX_ERRORCHECK: 提供錯誤檢查。若是某個線程嘗試從新鎖定的互斥鎖已經由該線程鎖定,則將返回錯誤。若是某個線程嘗試解除鎖定的互斥鎖不是由該線程鎖定或者未鎖定,則將返回錯誤。
  • PTHREAD_MUTEX_RECURSIVE:該互斥鎖會保留鎖定計數這一律念。線程首次成功獲取互斥鎖時,鎖定計數會設置爲 1。線程每從新鎖定該互斥鎖一次,鎖定計數就增長 1。線程每解除鎖定該互斥鎖一次,鎖定計數就減少 1。 鎖定計數達到 0 時,該互斥鎖便可供其餘線程獲取。若是某個線程嘗試解除鎖定的互斥鎖不是由該線程鎖定或者未鎖定,則將返回錯誤。
  • PTHREAD_MUTEX_DEFAULT: 嘗試以遞歸方式鎖定該互斥鎖將產生不肯定的行爲。對於不是由調用線程鎖定的互斥鎖,若是嘗試解除對它的鎖定,則會產生不肯定的行爲。若是嘗試解除鎖定還沒有鎖定的互斥鎖,則會產生不肯定的行爲。

1.2 @synchronized

一個便捷的建立互斥鎖的方式,它作了其餘互斥鎖所作的全部的事情。swift

@synchronized(object) 指令使用的 object 爲該鎖的惟一標識,只有當標識相同時,才知足互斥。若是你在不一樣的線程中傳過去的是同樣的標識符,先得到鎖的會鎖定代碼塊,另外一個線程將被阻塞,若是傳遞的是不一樣的標識符,則不會形成線程阻塞。數組

- (void)synchronized
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @synchronized(self) {
            sleep(2);
            NSLog(@"線程1");
        }
        NSLog(@"線程1解鎖成功");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        @synchronized(self) {
            NSLog(@"線程2");
        }
    });
}
打印:
2020-04-26 17:58:14.534038+0800 lock[3891:797979] 線程1
2020-04-26 17:58:14.534250+0800 lock[3891:797979] 線程1解鎖成功
2020-04-26 17:58:14.534255+0800 lock[3891:797981] 線程2
複製代碼

1.2.1 @synchronized 原理

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk  main.m
複製代碼

@synchronized(obj)clang編譯後的僞代碼以下:緩存

@try {
    objc_sync_enter(obj);
    // do work
} @finally {
    objc_sync_exit(obj);    
}
複製代碼

進入 objc4-756.2 源碼

數據結構

typedef struct SyncData {
    id object;
    recursive_mutex_t mutex;
    struct SyncData* nextData;
    int threadCount;
} SyncData;

typedef struct SyncList {
    SyncData *data;
    spinlock_t lock;
} SyncList;

// Use multiple parallel lists to decrease contention among unrelated objects.
#define COUNT 16
#define HASH(obj) ((((uintptr_t)(obj)) >> 5) & (COUNT - 1))
#define LOCK_FOR_OBJ(obj) sDataLists[HASH(obj)].lock
#define LIST_FOR_OBJ(obj) sDataLists[HASH(obj)].data
static SyncList sDataLists[COUNT];
複製代碼

SyncData 結構體 :

  • 傳入的 obj
  • obj 關聯的 recursive_mutex_t 鎖
  • 指向另外一個 SyncData 對象的指針 nextData,因此能夠把每一個 SyncData 結構體看作是鏈表中的一個節點。
  • 每一個 syncData 對象中的鎖會被一些線程使用或等待,threadCount就是此時這些線程的數量。syncData結構體 會被緩存,threadCount= 0 表明這個syncData實例能夠被複用.

SyncList 結構體:

  • SyncData 當作是鏈表中的節點,每一個 SyncList 結構體都有個指向 SyncData 節點鏈表頭部的指針,也有一個用於防止多個線程對此列表作併發修改的鎖。

sDataLists 結構體數組:

  • 一個 SyncList 結構體數組,大小爲16。經過定義的一個哈希算法將傳入對象映射到數組上的一個下標。值得注意的是這個哈希算法設計的很巧妙,是將對象指針在內存的地址轉化爲無符號整型並右移五位,再跟 0xF 作按位與運算,這樣結果不會超出數組大小。
  • LOCK_FOR_OBJ(obj) 和 LIST_FOR_OBJ(obj):先是哈希出對象的數組下標,而後取出數組對應元素的 lock 或 data。 LOCK_FOR_OBJ(obj)LIST_FOR_OBJ(obj)

當調用 objc_sync_enter(obj) 時,它用 obj 內存地址的哈希值查找合適的 SyncData,而後將其上鎖。

當調用 objc_sync_exit(obj) 時,它查找合適的 SyncData 並將其解鎖。

objc_sync_enter

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
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
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}

BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
);
複製代碼
  • 若是 obj = nil,@synchronized(nil) does nothing
  • 若是 obj 有值,runtime會爲傳入的 obj 分配一個 遞歸鎖並存儲在哈希表中
  • obj 經過 id2data(obj, ACQUIRE) 封裝成 SyncData(obj)
  • 遞歸鎖在被同一線程重複獲取時不會產生死鎖,因此遞歸鎖配合 @synchronized(nil) 保證被同一線程重複獲取時不會產生死鎖。不過雖然 nil 不行,但 @synchronized([NSNull null]) 是能夠的。

1.2.2 面試題

  1. 問題1: 下面的代碼運行會發生什麼?
- (void)synchronizedTest
{
    self.testArray = [NSMutableArray array];
    
    for (NSInteger i = 0; i < 200000; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            _testArray = [NSMutableArray array];
        });
    }
}
複製代碼
  • _testArray在不一樣的線程中不斷的 retain release,會存在某個時刻,多個線程同時對_testArray進行release,致使crash。
  1. 問題2:用 @synchronizing 鎖住 _testArray,還會crash麼?
- (void)synchronizedTest
{
    self.testArray = [NSMutableArray array];
    
    for (NSInteger i = 0; i < 200000; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (_testArray) {
                _testArray = [NSMutableArray array];
            }
        });
    }
    
}
複製代碼
  • 上面咱們學習了,@synchronized(nil) = do nothing,依然崩潰

被鎖對象爲nil時,@synchronized並不盡如人意,怎麼才能解決問題呢?使用NSLock。

{
    self.testArray = [NSMutableArray array];
    
    NSLock *lock = [[NSLock alloc] init];
    
    for (NSInteger i = 0; i < 200000; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            _testArray = [NSMutableArray array];
            [lock unlock];
        });
    }
    
}
複製代碼

1.3 NSLock

NSLock 底層pthread_mutex_lock 實現的, 屬性爲 PTHREAD_MUTEX_ERRORCHECK。遵循 NSLocking 協議。

@protocol NSLocking

- (void)lock;   
- (void)unlock;

@end

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end
複製代碼
  • lock:加鎖
  • unlock:解鎖
  • tryLock:嘗試加鎖,若是失敗的話返回 NO
  • lockBeforeDate: 在指定Date以前嘗試加鎖,若是在指定時間以前都不能加鎖,則返回NO
- (void)nslock
{
    NSLock *lock = [[NSLock alloc] init];
    //線程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [lock lock];
            NSLog(@"線程1");
            sleep(2);
            [lock unlock];
            NSLog(@"線程1解鎖成功");
    });

    //線程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            sleep(1);//以保證讓線程2的代碼後執行
            [lock lock];
            NSLog(@"線程2");
            [lock unlock];
    });
}
打印:
2020-04-26 20:27:36.474376+0800 lock[6554:889229] 線程1
2020-04-26 20:27:38.474856+0800 lock[6554:889229] 線程1解鎖成功
2020-04-26 20:27:38.474880+0800 lock[6554:889230] 線程2
複製代碼
  • 線程 1 中的 lock 鎖上了,因此線程 2 中的 lock 加鎖失敗,阻塞線程 2,但 2 s 後線程 1 中的 lock 解鎖,線程 2 就當即加鎖成功,執行線程 2 中的後續代碼。

1.4 NSRecursiveLock

  1. NSRecursiveLock 的底層是經過 pthread_mutex_lock 實現的,屬性爲 PTHREAD_MUTEX_RECURSIVE
  2. NSRecursiveLockNSLock 的區別在於:NSRecursiveLock 能夠在 同一個線程 中重複加鎖,NSRecursiveLock 會記錄上鎖和解鎖的次數,當兩者平衡的時候,纔會釋放鎖,其它線程才能夠上鎖成功。
@protocol NSLocking

- (void)lock;   
- (void)unlock;

@end

@interface NSRecursiveLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@end
複製代碼
  1. 應用場景
- (void)recursiveLock
{
    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        static void (^testMethod)(int);
        
        testMethod = ^(int value) {
            [lock lock];
            if (value > 0) {
                NSLog(@"current value = %d", value);
                testMethod(value - 1);
            }
            [lock unlock];
        };
        
        testMethod(10);
    });
}
打印:
2020-04-26 21:40:24.390756+0800 lock[6691:924076] current value = 10
2020-04-26 21:40:24.390875+0800 lock[6691:924076] current value = 9
2020-04-26 21:40:24.390956+0800 lock[6691:924076] current value = 8
2020-04-26 21:40:24.391043+0800 lock[6691:924076] current value = 7
2020-04-26 21:40:24.391131+0800 lock[6691:924076] current value = 6
2020-04-26 21:40:24.391211+0800 lock[6691:924076] current value = 5
2020-04-26 21:40:24.391295+0800 lock[6691:924076] current value = 4
2020-04-26 21:40:24.391394+0800 lock[6691:924076] current value = 3
2020-04-26 21:40:24.391477+0800 lock[6691:924076] current value = 2
2020-04-26 21:40:24.391561+0800 lock[6691:924076] current value = 1
複製代碼
  • 上面的示例,若是用 NSLock 的話,lock 先上鎖,但未執行解鎖的時候,就會進入遞歸的下一層再次請求上鎖,阻塞了該線程,線程被阻塞了,天然後面的解鎖代碼不會執行,而造成了死鎖。而 NSRecursiveLock 遞歸鎖就是爲了解決這個問題。

1.5 互斥鎖總結

對於 @synchronized NSLock NSRecursiveLock 應用場景的我的拙見,若是有問題請各位大佬指正:

  • 普通的線程安全場景,使用 NSLock 便可
  • 同一線程遞歸,使用 NSRecursiveLock
  • 多線程遞歸,更多的關注死鎖現象,建議使用 @synchronized (本質是對遞歸鎖的封裝,但可以防止一些死鎖, 使用時注意被鎖對象不能爲nil)

例以下面的代碼,在 for循環 中不斷建立線程,在各自的線程中又不斷 遞歸 ,這種多線程+遞歸的狀況下,使用@synchronized加鎖。

- (void)test
{
    for (int i= 0; i<100; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
           
            static void (^testMethod)(int);
            
            testMethod = ^(int value){
                
                @synchronized (self) {
                    if (value > 0) {
                      NSLog(@"current value = %d",value);
                      testMethod(value - 1);
                    }
                }
                
            };
            testMethod(10);
        });
    }
}
複製代碼

二. 自旋鎖

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

  1. 自旋鎖與互斥鎖功能同樣,惟一不一樣的就是:
  • 互斥鎖阻塞後休眠讓出cpu
  • 自旋鎖阻塞後不會讓出cpu,會一直忙等((busy-wait)待,直到獲得鎖
  1. 應用場景:
  • 在用戶態使用的比較少,在內核使用的比較多
  • 鎖的持有時間比較短,或者說小於2次上下文切換的時間。
  1. 自旋鎖的API和互斥鎖類似,把 pthread_mutex_xxx()mutex 換成 spin,如:pthread_spin_init()

  2. 自旋鎖目前已不安全,可能會出現優先級翻轉問題。假設有三個準備執行的任務A、B、C 和 須要互斥訪問的共享資源S,三個任務的優先級依次是 A > B > C;

  • 首先:C處於運行狀態,得到CPU正在執行,同時佔有了資源S;
  • 其次:A進入就緒狀態,由於優先級比C高,因此得到CPU,A轉爲運行狀態;C進入就緒狀態;
  • 第三:執行過程當中須要使用資源,而這個資源又被等待中的C佔有的,因而A進入阻塞狀態,C回到運行狀態;
  • 第四:此時B進入就緒狀態,由於優先級比C高,B得到CPU,進入運行狀態;C又回到就緒狀態;
  • 第五:若是這時又出現B2,B3等任務,他們的優先級比C高,但比A低,那麼就會出現高優先級任務的A不能執行,反而低優先級的B,B2,B3等任務能夠執行的奇怪現象,而這就是優先反轉。

atomic 底層原理

說到自旋鎖,不得不提屬性修飾符 atomic。

1. setter方法底層原理 -- reallySetProperty

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) {
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset) {
    reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}

void objc_setProperty_nonatomic(id self, SEL _cmd, id newValue, ptrdiff_t offset) {
    reallySetProperty(self, _cmd, newValue, offset, false, false, false);
}

void objc_setProperty_atomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset) {
    reallySetProperty(self, _cmd, newValue, offset, true, true, false);
}

void objc_setProperty_nonatomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset) {
    reallySetProperty(self, _cmd, newValue, offset, false, true, false);
}
複製代碼
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);
}
複製代碼
  • 若是屬性是非原子屬性的:直接 newValue 替換 oldValue
  • 若是屬性是原子屬性的:建立一個 spinlock_t 類型的鎖,並給鎖加鹽。在鎖環境下 newValue 替換 oldValue

2. getter方法底層原理 -- objc_getProperty

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);
}
複製代碼
  • 若是是非原子屬性的,直接返回鹽地址下的值
  • 若是是原子屬性的,在鎖環境下取值

3. spinlock_t& slotlock = PropertyLocks[slot] 究竟是什麼類型的鎖?

spinlock_t 看名字很像自旋鎖,可是自旋鎖已經不安全了。來看下 spinlock_t 的定義

using spinlock_t = mutex_tt<DEBUG>;
using mutex_locker_t = mutex_tt<LOCKDEBUG>::locker;
複製代碼

看來,蘋果在底層使用 mutex_locker_t 替換了 spinlock_tmutex_locker_t 又是什麼?

/*!
 * @typedef os_unfair_lock
 *
 * @abstract
 * Low-level lock that allows waiters to block efficiently on contention.
 *
 * In general, higher level synchronization primitives such as those provided by
 * the pthread or dispatch subsystems should be preferred.
 *
 * The values stored in the lock should be considered opaque and implementation
 * defined, they contain thread ownership information that the system may use
 * to attempt to resolve priority inversions.
 *
 * This lock must be unlocked from the same thread that locked it, attemps to
 * unlock from a different thread will cause an assertion aborting the process.
 *
 * This lock must not be accessed from multiple processes or threads via shared
 * or multiply-mapped memory, the lock implementation relies on the address of
 * the lock value and owning process.
 *
 * Must be initialized with OS_UNFAIR_LOCK_INIT
 *
 * @discussion
 * Replacement for the deprecated OSSpinLock. Does not spin on contention but
 * waits in the kernel to be woken up by an unlock.
 *
 * As with OSSpinLock there is no attempt at fairness or lock ordering, e.g. an
 * unlocker can potentially immediately reacquire the lock before a woken up
 * waiter gets an opportunity to attempt to acquire the lock. This may be
 * advantageous for performance reasons, but also makes starvation of waiters a
 * possibility.
 */
OS_UNFAIR_LOCK_AVAILABILITY
typedef struct os_unfair_lock_s {
    uint32_t _os_unfair_lock_opaque;
} os_unfair_lock, *os_unfair_lock_t;
複製代碼

仍是要讚歎下蘋果官方註釋,太詳細了。

  • os_unfair_lock 是一種低級鎖,必須在 OS_UNFAIR_LOCK_INIT 下初始化。
  • 通常來講,應該首選更高級別的同步工具,如 pthread 或 dispatch 子系統提供的同步工具。
  • 鎖裏面包含線程全部權信息,用來解決優先級反轉問題
  • 該鎖必須從鎖定它的 同一線程 解除鎖定,嘗試從其餘線程解除鎖定將致使斷言停止進程。
  • 不能經過共享或多重映射內存從 多個進程或線程 訪問此鎖,鎖的實現依賴於鎖值和所屬進程的地址。
  • 用來代替廢棄的 OSSpinLock(iOS 10廢棄)。
  • 出於性能的考慮,解鎖器可能會在醒來以前當即從新獲取鎖。

4. atomic 必定是線程安全的麼?

atomic 會對屬性的 setter方法 、getter方法 分別加鎖,生成了原子性的 setter、getter。這裏的原子性也就意味着:假設當前有兩個線程,線程A執行 getter 方法的時候,線程B若是想要執行 setter 方法,必需要等到getter方法執行完畢以後才能執行。

簡而言之,atomic只能保證代碼進入 getter 或者 setter 函數內部是安全的,一旦出現了同時getter 和 setter,多線程只能靠程序員本身保證。因此atomic屬性和使用@property的多線程安全沒有直接的聯繫。

舉個例子:線程A 和 線程B 都對屬性 num 執行10000次 + 1 操做。若是線程安全的話,程序運行結束後,num的值應該是20000。

@property (atomic, assign) NSInteger num;

- (void)atomicTest {
    //Thread A
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10000; i ++) {
            self.num = self.num + 1;
            NSLog(@"%@ -- %ld", [NSThread currentThread], (long)self.num);
        }
    });
    
    //Thread B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10000; i ++) {
            self.num = self.num + 1;
             NSLog(@"%@ -- %ld", [NSThread currentThread], (long)self.num);
        }
    });
}
打印:
···
2020-04-28 16:35:55.126996+0800 lock[10384:1662304] <NSThread: 0x600000ea3c00>{number = 3, name = (null)} -- 19994
2020-04-28 16:35:55.127083+0800 lock[10384:1662299] <NSThread: 0x600000eecdc0>{number = 5, name = (null)} -- 19995
2020-04-28 16:35:55.127165+0800 lock[10384:1662304] <NSThread: 0x600000ea3c00>{number = 3, name = (null)} -- 19996
2020-04-28 16:35:55.127250+0800 lock[10384:1662299] <NSThread: 0x600000eecdc0>{number = 5, name = (null)} -- 19997
2020-04-28 16:35:55.127341+0800 lock[10384:1662304] <NSThread: 0x600000ea3c00>{number = 3, name = (null)} -- 19998
複製代碼

self.num = self.num + 1 方法:

  • 等號左邊 self.num 調用 setter 方法,是原子屬性的
  • 等號右邊 self.num 調用 getter 方法,是原子屬性的
  • 可是 self.num + 1 不是原子屬性的啊,仍是會出現線程問題。

另外,atomic因爲要鎖住該屬性,所以它會消耗更多的資源,性能會很低,要比 nonatomic 慢20倍。因此iOS移動端開發,咱們通常使用nonatomic。可是在mac開發中,atomic就有意義了。

三. 讀寫鎖

讀寫鎖實際是一種特殊的自旋鎖,它把對共享資源的訪問者劃分紅讀者和寫者,讀者只對共享資源進行讀訪問,寫者則須要對共享資源進行寫操做。這種鎖相對於自旋鎖而言,能提升併發性,由於在多處理器系統中,它容許同時有多個讀者來訪問共享資源,最大可能的讀者數爲實際的邏輯CPU數。寫者是排他性的,一個讀寫鎖同時只能有一個寫者或多個讀者(與CPU數相關),但不能同時既有讀者又有寫者

  1. 讀寫鎖與互斥鎖相似,不過讀寫鎖容許更改的並行性,也叫共享互斥鎖
  • 互斥鎖要麼是鎖住狀態,要麼就是不加鎖狀態,並且一次只有一個線程能夠對其加鎖。
  • 讀寫鎖能夠有3種狀態:讀模式下加鎖狀態寫模式加鎖狀態不加鎖狀態
  1. 讀寫鎖的特色: 多讀單寫
  • 一次只有一個線程能夠佔有寫模式的讀寫鎖, 可是能夠有多個線程同時佔有讀模式的讀寫鎖. 正是由於這個特性,
  • 當讀寫鎖是寫加鎖狀態時, 在這個鎖被解鎖以前, 全部試圖對這個鎖加鎖的線程都會被阻塞.
  • 當讀寫鎖在讀加鎖狀態時, 全部試圖以讀模式對它進行加鎖的線程均可以獲得訪問權, - 可是若是線程但願以寫模式對此鎖進行加鎖, 它必須直到全部的線程釋放鎖.
  1. 讀寫鎖的API:
#include <pthread.h>
// 初始化讀寫鎖
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); 

// 申請讀鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock ); 

// 申請寫鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock ); 

// 嘗試以非阻塞的方式來在讀寫鎖上獲取寫鎖。若是有任何的讀者或寫者持有該鎖,則當即失敗返回。
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); 

// 解鎖
int pthread_rwlock_unlock (pthread_rwlock_t *rwlock); 

// 銷燬讀寫鎖
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
複製代碼
  1. 應用場景:
  • 讀寫鎖適合於對數據結構的讀次數比寫次數多得多的狀況。
// 用於讀寫的併發隊列:
@property (nonatomic, strong) dispatch_queue_t concurrent_queue;
// 用戶數據中心, 可能多個線程須要數據訪問:
@property (nonatomic, strong) NSMutableDictionary *dataCenterDic;

- (void)readWriteTest
{
    self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
    self.dataCenterDic = [NSMutableDictionary dictionary];
    
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    // 模擬多線程狀況下寫
    for (NSInteger i = 0; i < 5; i ++) {
        dispatch_async(queue, ^{
            [self ak_setObject:[NSString stringWithFormat:@"akironer--%ld", (long)i] forKey:@"Key"];
        });
    }
    
    // 模擬多線程狀況下讀
    for (NSInteger i = 0; i < 20; i ++) {
        dispatch_async(queue, ^{
            [self ak_objectForKey:@"Key"];
        });
    }
    
    // 模擬多線程狀況下寫
    for (NSInteger i = 0; i < 10; i ++) {
        dispatch_async(queue, ^{
            [self ak_setObject:[NSString stringWithFormat:@"iOS--%ld", (long)i] forKey:@"Key"];
        });
    }
}

#pragma mark - 讀數據
- (id)ak_objectForKey:(NSString *)key {
    __block id obj;
    // 同步讀取數據:
    dispatch_sync(self.concurrent_queue, ^{
        obj = [self.dataCenterDic objectForKey:key];
        NSLog(@"讀:%@--%@", obj, [NSThread currentThread]);
        sleep(1);
    });
    return obj;
}

#pragma mark - 寫數據
- (void)ak_setObject:(id)obj forKey:(NSString *)key {
    // 異步柵欄調用設置數據: 屏蔽同步
    dispatch_barrier_async(self.concurrent_queue, ^{
        [self.dataCenterDic setObject:obj forKey:key];
        NSLog(@"寫:%@--%@", obj, [NSThread currentThread]);
        sleep(1);
    });
}
複製代碼

四. 條件鎖

  1. 與互斥鎖不一樣,條件鎖是用來等待而不是用來上鎖的。條件鎖用來自動阻塞一個線程,直 到某特殊狀況發生爲止。一般條件鎖和互斥鎖通常同時使用。

  2. 條件鎖是利用線程間共享的全局變量進行同步 的一種機制,使咱們能夠睡眠等待某種條件出現,主要包括兩個動做:

  • 一個線程等待 "條件鎖的條件成立" 而掛起;
  • 另外一個線程使 「條件成立」(給出條件成立信號)。
  1. 條件鎖的三要素:
  • 互斥鎖:當檢測條件時保護數據源,執行條件引起的任務
  • 條件變量:判斷條件是否知足的依據
  • 條件探測變量:根據條件決定是否繼續運行線程,即線程是否被阻塞

4.1 NSCondition 條件變量

  1. NSCondition 的底層經過 pthread_cond_t 實現的。NSCondition 的對象實際上做爲一個鎖和一個線程檢查器
  • 鎖:當檢測條件時保護數據源,執行條件引起的任務;
  • 線程檢查器:根據條件決定是否繼續運行線程,即線程是否被阻塞
  1. NSCondition 實現了 NSLocking協議,當多個線程訪問同一段代碼時,會以 wait 爲分水嶺。一個線程等待另外一個線程 unlock 以後,再走 wait 以後的代碼。
@protocol NSLocking

- (void)lock;   
- (void)unlock;

@end

@interface NSCondition : NSObject <NSLocking> {
@private
    void *_priv;
}

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@end
複製代碼
  • lock: 通常用於多線程同時訪問、修改同一個數據源,保證在同一 時間內數據源只被訪問、修改一次,其餘線程的命令須要在lock 外等待,只到 unlock ,纔可訪問
  • unlock: 解鎖
  • wait:讓當前線程處於等待狀態
  • signal:任意通知一個線程
  • broadcast:通知全部等待的線程
  1. 應用場景:生產者-消費者模式:
  • 生產模式下,商品數量 + 1
  • 消費模式下,商品數量 - 1
  • 如何保證消費模式下商品數量大於零呢?
- (void)testConditon
{
    self.testCondition = [[NSCondition alloc] init];
    //建立生產-消費者
    for (int i = 0; i < 10; i++) {
        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]; // 消費
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self consumer]; // 消費
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self producer]; // 生產
        });
        
    }
}

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

- (void)consumer
{
    // 線程安全
    [self.testCondition lock];

    while (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        // 保證正常流程
        [self.testCondition wait];
    }
    
    //注意消費行爲,要在等待條件判斷以後
    self.ticketCount -= 1;
    NSLog(@"消費一個 還剩 count %zd ",self.ticketCount);
    [self.testCondition unlock];
}
打印:
2020-04-27 17:46:43.232762+0800 lock[7444:1140032] 生產一個 現有 count 1
2020-04-27 17:46:43.232900+0800 lock[7444:1140032] 生產一個 現有 count 2
2020-04-27 17:46:43.233001+0800 lock[7444:1140032] 消費一個 還剩 count 1 
2020-04-27 17:46:43.233109+0800 lock[7444:1140066] 消費一個 還剩 count 0 
2020-04-27 17:46:43.233209+0800 lock[7444:1140070] 等待 count 0
2020-04-27 17:46:43.233308+0800 lock[7444:1140030] 等待 count 0
2020-04-27 17:46:43.233406+0800 lock[7444:1140057] 等待 count 0
2020-04-27 17:46:43.233508+0800 lock[7444:1140058] 生產一個 現有 count 1
2020-04-27 17:46:43.233611+0800 lock[7444:1140070] 消費一個 還剩 count 0 
2020-04-27 17:46:43.233713+0800 lock[7444:1140059] 等待 count 0
2020-04-27 17:46:43.234100+0800 lock[7444:1140061] 生產一個 現有 count 1
2020-04-27 17:46:43.234343+0800 lock[7444:1140030] 消費一個 還剩 count 0 
複製代碼

4.2 NSConditionLock 條件鎖

  1. NSConditionLock 藉助 NSCondition 來實現,它的本質就是一個生產者-消費者模型。NSConditionLock 的內部持有一個 NSCondition 對象,以及 _condition_value 屬性,在初始化時就會對這個屬性進行賦值.

  2. NSConditionLock 實現了 NSLocking協議,一個線程會等待另外一個線程 unlock 或者 unlockWithCondition: 以後再走 lock 或者 lockWhenCondition: 以後的代碼。

  3. 相比於 NSCondition, NSConditonLock 自帶一個條件探測變量,使用更加靈活。

@protocol NSLocking

- (void)lock;   
- (void)unlock;

@end

@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end
複製代碼
  • lock : 表示 xxx 期待得到鎖,若是沒有其餘線程得到鎖(不須要判斷內部的 condition條件) 那它能執行此行如下代碼若是已經有其餘線程得到鎖(多是條件鎖,或者無條件 鎖),則等待,直至其餘線程解鎖
  • condition:內部condition條件。這屬性很是重要,外部condition條件內部condition條件 相同纔會獲取到 lock 對象;反之阻塞當前線程,直到condition相同
  • lockWhenCondition:(NSInteger)conditionA表示在沒有其餘線程得到該鎖的前提下該鎖 內部condition條件 不等於 條件A,不能得到鎖,仍然等待若是鎖 內部condition 等於A條件,則進入代碼區,同時設置它得到該鎖,其餘任何線程都將等待它代碼 的完成,直至它解鎖
  • unlockWithCondition:(NSInteger)conditionA: 表示釋放鎖,同時把 內部condition條件 設置爲A條件
  • return = lockWhenCondition:(NSInteger)conditionA beforeDate:(NSDate *)limitA:表示若是被鎖定(沒得到 鎖),並超過 時間A 則再也不阻塞線程。可是注意: 返回的值是NO, 它沒有改變鎖的狀態,這個函數的目的在於能夠實現兩種狀態下的處理
- (void)testConditonLock
{
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
       [conditionLock lockWhenCondition:1];
       NSLog(@"線程 1");
       [conditionLock unlockWithCondition:0];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
       [conditionLock lockWhenCondition:2];
       NSLog(@"線程 2");
       [conditionLock unlockWithCondition:1];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       [conditionLock lock];
       NSLog(@"線程 3");
       [conditionLock unlock];
    });
}
打印:
2020-04-27 18:00:21.876356+0800 lock[7484:1148383] 線程 3
2020-04-27 18:00:21.876629+0800 lock[7484:1148384] 線程 2
2020-04-27 18:00:21.876751+0800 lock[7484:1148386] 線程 1
複製代碼
  • 線程 1 調用 [NSConditionLock lockWhenCondition:1] ,此時由於不知足當前條件,所 以會進入 waiting 狀態,當前進入到 waiting 時,會釋放當前的互斥鎖。
  • 此時線程 3 調用 [NSConditionLock lock],本質上是調用 [NSConditionLock lockBeforeDate:],這裏不須要比對條件值,因此線程 3 會打印
  • 接下來線程 2 執行 [NSConditionLock lockWhenCondition:2],由於知足條件值,因此線程 2 會打印,打印完成後會調用 [NSConditionLock unlockWithCondition:1] 將 value 設置爲 1,併發送 boradcast
  • 線程 1 接收到當前的信號,喚醒執行並打印。
  • 自此當前打印爲 線程 3->線程 2 -> 線程 1。
  • [NSConditionLock lockWhenCondition:] 會根據傳入的 condition 值和 Value 值進 行對比,若是不相等,這裏就會阻塞,進入線程池,不然的話就繼續代碼執行
  • [NSConditionLock unlockWithCondition:] 會先更改當前的 value 值,而後進行廣 播,喚醒當前的線程。

五. 信號量

信號量普遍用於進程或線程間的同步和互斥,信號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。

#include <semaphore.h>
// 初始化信號量
int sem_init(sem_t *sem, int pshared, unsigned int value);

// 信號量 P 操做(減 1)
int sem_wait(sem_t *sem);

// 以非阻塞的方式來對信號量進行減 1 操做
int sem_trywait(sem_t *sem);

// 信號量 V 操做(加 1)
int sem_post(sem_t *sem);

// 獲取信號量的值
int sem_getvalue(sem_t *sem, int *sval);

// 銷燬信號量
int sem_destroy(sem_t *sem);
複製代碼

GCD 的 dispatch_semaphore,能夠參考iOS進階之路 (十六)多線程 - GCD

六:總結

在 ibireme 大神的 再也不安全的 OSSpinLock中,對各類鎖的性能作了測試(加鎖後當即解鎖,並無計算競爭時候的時間消耗)

  • OSSpinLock 性能最高,但它已經再也不安全。
  • @synchronized 的效率最低,相信學習了本篇文章,@synchronized 再也不是加鎖的首先。

參考資料

Cooci -- iOS 中的八大鎖

bestswifter -- 深刻理解iOS開發中的鎖

王令天下 -- 關於 @synchronized,這兒比你想知道的還要多

相關文章
相關標籤/搜索