通过之前篇章的学习,我们对整个GCD从使用到原理,都有了一定的理解。这篇主要讲解一下iOS开发中的锁是什么情况
系列文章传送门:
基础小概念
什么是锁
锁 -- 是保证线程安全常见的同步工具。锁是一种非强制的机制,每一个线程在访问数据或者资源前,要先获取(Acquire
) 锁,并在访问结束之后释放(Release
)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。
锁的作用
前面说到了,锁是用来保护线程安全的工具。
可以试想一下,多线程编程时,没有锁的情况 -- 也就是线程不安全。
当多个线程同时对一块内存发生读和写的操作,可能出现意料之外的结果:
程序执行的顺序会被打乱,可能造成提前释放一个变量,计算结果错误等情况。
所以我们需要将线程不安全的代码 “锁” 起来。保证一段代码或者多段代码操作的原子性,保证多个线程对同一个数据的访问 同步 (Synchronization
)。
锁的分类
锁的分类方式,可以根据锁的状态,锁的特性等进行不同的分类,很多锁之间其实并不是并列的关系,而是一种锁下的不同实现。可以看这篇文章JAVA中锁的分类
互斥锁与自旋锁
互斥锁:是⼀种⽤于多线程编程中,防⽌两条线程同时对同⼀公共资源(⽐ 如全局变量)进⾏读写的机制。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。 互斥锁又分为递归锁和非递归锁。
- 递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用。
- 非递归锁:不可重入,必须等锁释放后才能再次获取锁。
⾃旋锁:线程反复检查锁变量是否可⽤。由于线程在这⼀过程中保持执⾏, 因此是⼀种忙等待。⼀旦获取了⾃旋锁,线程会⼀直保持该锁,直⾄显式释 放⾃旋锁。 ⾃旋锁避免了进程上下⽂的调度开销,因此对于线程只会阻塞很 短时间的场合是有效的。
互斥锁与自旋锁区别:
其实就是线程的区别,互斥锁在线程获取锁但没有获取到时,线程会进入休眠状态,等锁被释放时,线程会被唤醒,而自旋锁的线程则会一直处于等待状态,忙等待,不会进入休眠。
自旋锁
1. OSSpinLock
相信大家都拜读过这片文章->不再安全的 OSSpinLock。总结来说,自旋锁之所以不安全,是因为由于自旋锁获取锁时,线程会一直处于忙等待状态,造成了任务的优先级反转。
而 OSSpinLock
忙等的机制,就可能造成高优先级一直 running
,占用 CPU
时间片。而低优先级任务无法抢占时间片,变成迟迟完不成,不释放锁的情况。
2. atomic
在面试中,我们经常遇到关于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
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_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
结构后,我们继续来查看源码,由于源码比较长,所以我们分模块俩讲解。
1.1 准备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;
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
是对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];
}
5. NSConditionLock
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
6. os_unfair_lock
由于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
类似