通过之前篇章的学习,我们对整个GCD从使用到原理,都有了一定的理解。这篇主要讲解一下iOS开发中的锁是什么情况
系列文章传送门:
☞ iOS底层学习 - 多线程之基础原理篇
☞ iOS底层学习 - 多线程之GCD初探
☞ iOS底层学习 - 多线程之GCD队列原理篇
☞ iOS底层学习 - 多线程之GCD应用篇
☞ iOS底层学习 - 多线程之GCD底层原理篇
锁 -- 是保证线程安全常见的同步工具。锁是一种非强制的机制,每一个线程在访问数据或者资源前,要先获取(Acquire
) 锁,并在访问结束之后释放(Release
)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。
前面说到了,锁是用来保护线程安全的工具。
可以试想一下,多线程编程时,没有锁的情况 -- 也就是线程不安全。
当多个线程同时对一块内存发生读和写的操作,可能出现意料之外的结果:
程序执行的顺序会被打乱,可能造成提前释放一个变量,计算结果错误等情况。
所以我们需要将线程不安全的代码 “锁” 起来。保证一段代码或者多段代码操作的原子性,保证多个线程对同一个数据的访问 同步 (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
类似iOS 的锁
iOS 锁的底层探索笔记