經過以前篇章的學習,咱們對整個GCD從使用到原理,都有了必定的理解。這篇主要講解一下iOS開發中的鎖是什麼狀況html
系列文章傳送門:ios
☞ iOS底層學習 - 多線程之GCD隊列原理篇swift
鎖 -- 是保證線程安全常見的同步工具。鎖是一種非強制的機制,每個線程在訪問數據或者資源前,要先獲取(Acquire
) 鎖,並在訪問結束以後釋放(Release
)鎖。若是鎖已經被佔用,其它試圖獲取鎖的線程會等待,直到鎖從新可用。安全
前面說到了,鎖是用來保護線程安全的工具。markdown
能夠試想一下,多線程編程時,沒有鎖的狀況 -- 也就是線程不安全。多線程
當多個線程同時對一塊內存發生讀和寫的操做,可能出現意料以外的結果:
程序執行的順序會被打亂,可能形成提早釋放一個變量,計算結果錯誤等狀況。
因此咱們須要將線程不安全的代碼 「鎖」 起來。保證一段代碼或者多段代碼操做的原子性,保證多個線程對同一個數據的訪問 同步 (Synchronization
)。
鎖的分類方式,能夠根據鎖的狀態,鎖的特性等進行不一樣的分類,不少鎖之間其實並非並列的關係,而是一種鎖下的不一樣實現。能夠看這篇文章JAVA中鎖的分類
互斥鎖:是⼀種⽤於多線程編程中,防⽌兩條線程同時對同⼀公共資源(⽐ 如全局變量)進⾏讀寫的機制。當獲取鎖操做失敗時,線程會進入睡眠,等待鎖釋放時被喚醒。 互斥鎖又分爲遞歸鎖和非遞歸鎖。
⾃旋鎖:線程反覆檢查鎖變量是否可⽤。因爲線程在這⼀過程當中保持執⾏, 所以是⼀種忙等待。⼀旦獲取了⾃旋鎖,線程會⼀直保持該鎖,直⾄顯式釋 放⾃旋鎖。 ⾃旋鎖避免了進程上下⽂的調度開銷,所以對於線程只會阻塞很 短期的場合是有效的。
其實就是線程的區別,互斥鎖在線程獲取鎖但沒有獲取到時,線程會進入休眠狀態,等鎖被釋放時,線程會被喚醒,而自旋鎖的線程則會一直處於等待狀態,忙等待,不會進入休眠。
相信你們都拜讀過這片文章->再也不安全的 OSSpinLock。總結來講,自旋鎖之因此不安全,是由於因爲自旋鎖獲取鎖時,線程會一直處於忙等待狀態,形成了任務的優先級反轉。
而 OSSpinLock
忙等的機制,就可能形成高優先級一直 running
,佔用 CPU
時間片。而低優先級任務沒法搶佔時間片,變成遲遲完不成,不釋放鎖的狀況。
在面試中,咱們常常遇到關於atomic
相關的問題,總結來講主要是兩個方面,一個是atomic
的底層原理是怎樣的,另外一個是使用atomic
是否就能保證線程安全。
關於底層原理,咱們仍是來看源碼進行探索。經過源碼,咱們能夠發現,在方法的set
和get
方法中,會有是不是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
是保證set
和get
方法安全的,那是否是就說明其線程安全呢?其實並非的,這隻能保證該屬性在單一線程上是安全的,若是是有不少的線程對該屬性進行同時的操做,那麼就不能保證其數據安全了.好比下面的代碼,經過結果咱們能夠看到,並無起到加鎖的效果。
//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
複製代碼
讀寫鎖實際是⼀種特殊的⾃旋鎖,它把對共享資源的訪問者劃分紅讀者和寫者,讀者只對共享資源進⾏讀訪問,寫者則須要對共享資源進⾏寫操做。這種鎖相對於⾃旋鎖⽽⾔,能提⾼併發性,由於在多處理器系統中,它容許同時有多個讀者來訪問共享資源,最⼤可能的讀者數爲實際的邏輯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
複製代碼
由於互斥鎖出現優先級反轉後,高優先級的任務不會忙等。由於處於等待狀態的高優先級任務,沒有佔用時間片,因此低優先級任務通常都能進行下去,從而釋放掉鎖。
@synchronized
的使用很是簡單,代碼以下,傳入一個想要加鎖的對象,在其中執行加鎖的相關邏輯便可。
@synchronized (obj) {}
複製代碼
那麼其底層邏輯是如何實現的呢,咱們能夠看一下@synchronized
的源碼,經過打斷點,查看其彙編源碼,發現@synchronized
就是實現了objc_sync_enter
和 objc_sync_exit
兩個方法,也就是說是經過這兩個方法來實現加鎖和解鎖操做的。經過符號斷點,咱們能夠知道其代碼在objc
源碼中。
首先注意enter
和exit
中都首先對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
結構後,咱們繼續來查看源碼,因爲源碼比較長,因此咱們分模塊倆講解。
SyncData
咱們能夠看到會會經過LOCK_FOR_OBJ
和LIST_FOR_OBJ
取出object
所對應的lockp
和listp
。
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;
複製代碼
此步操做會經過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
...
}
複製代碼
這步操做是檢查全部線程中的緩存
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;
}
}
...
}
複製代碼
若是上述兩步中,單個線程和已經鎖住的線程中的緩存數據都沒有找到的話,那麼就會來到此步,回來系統保存的哈希表中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;
}
}
...
}
複製代碼
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不能爲空,不然沒法進行鎖操做。
相關信號量的底層原理,再上一章節已經講過,能夠直接查看☞iOS底層學習 - 多線程之GCD底層原理篇
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);
});
複製代碼
將上面例子中的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來解決
複製代碼
NSCondition
是一個條件鎖。
在線程間的同步中,有這樣一種狀況: 線程 A 須要等條件 C 成立,才能繼續往下執行.如今這個條件不成立,線程 A 就阻塞等待. 而線程 B 在執行過程當中,使條件 C 成立了,就喚醒線程 A 繼續執行。這個時候,咱們可使用條件鎖來完成相關邏輯。
條件鎖的底層實現其實就是一個互斥鎖和條件變量的封裝,因爲未開源,咱們仍是先看Swift源碼。
NSCondition
是對mutex
和cond
的一種封裝。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];
}
複製代碼
NSConditionLock
。咱們能夠經過Swift源碼查看可得
NSConditionLock
是NSCondition
加線程數的封裝,繼承NSLocking
協議,也有lock
和unlock
等方法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
複製代碼
因爲OSSpinLock
自旋鎖的bug,在iOS10以後OSSpinLock被廢棄,內部封裝了os_unfair_lock
,而os_unfair_lock
在加鎖時會處於休眠狀態,而不是自旋鎖的忙等狀態。
OSSpinLock
之因此不在安全,是由於自旋鎖會在線程等待時處於忙等狀態,會形成任務優先級翻轉,卻是沒法執行,目前用os_unfair_lock
來替代,是一個互斥鎖,互斥鎖不會處於忙等,不佔用時間片。atomic
底層實現原理就是對get
和set
方法進行加鎖,可是不能保證多條線程調用或者不適用get
和set
的線程安全,且性能消耗巨大併發隊列+dispatch_barrier_async
的方法,來實現一個相似的讀寫鎖@synchronized
要注意傳入的對象不能爲nil
,不然沒法加鎖。底層邏輯是維護了一個全局的哈希表用來存儲對象和鎖,會按照緩存線程->全部線程->全局哈希表
的方式進行增刪改查NSLock
是對pthread_mutex
的封裝,可是沒有遞歸邏輯。對同一個線程屢次lock
會形成阻塞。NSRecursiveLock
是在NSLock
的基礎上添加了遞歸邏輯,當只有一個遞歸鎖對象,多線程進行鎖操做時,會形成死鎖,可用@synchronized
解決NSCondition
和NSConditionLock
是條件鎖,當知足某一個條件時,才能進行操做,適用於生產者消費者模式,和信號量dispatch_semaphore
相似