一、@synchronized
锁是最常用的同步工具。一段代码段在同一个时间只能允许被有限个线程访问,比如一个线程 A 进入需要保护代码之前添加简单的互斥锁,另一个线程 B 就无法访问,只有等待前一个线程 A 执行完被保护的代码后解锁,B 线程才能访问被保护代码。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.ticketCount = 20;
[self lg_testSaleTicket];
}
- (void)lg_testSaleTicket{
for (int i = 0; i < 3; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 3; i++) {
[self saleTicket];
}
});
}
}
- (void)saleTicket{
// 枷锁 - 线程安全
@synchronized (self) {
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%ld张",self.ticketCount);
}else{
NSLog(@"当前车票已售罄");
}
}
}
输出结果
2020-03-03 20:02:28.275159+0800 001-@synchronized分析[85117:4216359] 当前余票还剩:19张
2020-03-03 20:02:28.275355+0800 001-@synchronized分析[85117:4216359] 当前余票还剩:18张
2020-03-03 20:02:28.275593+0800 001-@synchronized分析[85117:4216359] 当前余票还剩:17张
2020-03-03 20:02:28.275720+0800 001-@synchronized分析[85117:4216358] 当前余票还剩:16张
2020-03-03 20:02:28.275830+0800 001-@synchronized分析[85117:4216358] 当前余票还剩:15张
2020-03-03 20:02:28.275947+0800 001-@synchronized分析[85117:4216358] 当前余票还剩:14张
2020-03-03 20:02:28.277016+0800 001-@synchronized分析[85117:4216356] 当前余票还剩:13张
2020-03-03 20:02:28.277139+0800 001-@synchronized分析[85117:4216356] 当前余票还剩:12张
2020-03-03 20:02:28.277616+0800 001-@synchronized分析[85117:4216356] 当前余票还剩:11张
上面代码,假如我们在saleTicket方法中没有添加@synchronized输出就是:
2020-03-03 20:04:06.822674+0800 001-@synchronized分析[85214:4219574] 当前余票还剩:19张
2020-03-03 20:04:06.822721+0800 001-@synchronized分析[85214:4219575] 当前余票还剩:18张
2020-03-03 20:04:06.822852+0800 001-@synchronized分析[85214:4219580] 当前余票还剩:17张
2020-03-03 20:04:06.822936+0800 001-@synchronized分析[85214:4219575] 当前余票还剩:15张
2020-03-03 20:04:06.822935+0800 001-@synchronized分析[85214:4219574] 当前余票还剩:15张
2020-03-03 20:04:06.822959+0800 001-@synchronized分析[85214:4219580] 当前余票还剩:14张
2020-03-03 20:04:06.823059+0800 001-@synchronized分析[85214:4219575] 当前余票还剩:13张
2020-03-03 20:04:06.823085+0800 001-@synchronized分析[85214:4219580] 当前余票还剩:12张
2020-03-03 20:04:06.823091+0800 001-@synchronized分析[85214:4219574] 当前余票还剩:11张
里面会发现,第15张票造成了资源抢夺现象,这种情况就叫做线程不安全,当加锁以后就买票顺序就很正常了。
下面我们来分析下@synchronized是如何对代码进行加锁的,首先我们打开汇编,然后在@synchronized地方加一个断点:
我们在汇编里面发下了objc_sync_enter这个函数
并且在下面发现了objc_sync_exit函数
我们猜测在objc_sync_enter和objc_sync_exit之间就是加锁的一块代码
我们在main.m中添加@synchronized,然后通过clang命令将.m转成.cpp文件,
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
然后我们打开.cpp文件,我们发现被编译成这个代码
我们知道加锁代码是在objc_sync_enter和objc_sync_exit之间执行的,我们可以查看一下这两个函数的具体实现,通过打符号断点查看一下这两个函数实现在哪
我们发现objc_sync_enter其实是在libobjc.dylib库里面,也就是objc的源码,我们打开objc的源码搜索一下objc_sync_enter,找到它的具体实现
根据上面注释我们知道@synchronized是基于obj的递归锁
- 其实递归锁是一种特殊的互斥锁
根据实现我们发现当obj为nil时候,会调用objc_sync_nil();,我们再看一下**objc_sync_nil();**的实现,
BREAKPOINT_FUNCTION(
void objc_sync_nil(void)
);
并且根据注释** @synchronized(nil) does nothing**我们知道当obj为nil的时候其实什么事情都没做,所以我们得出结论,
当@synchronized(nil){}时候,其实是没有加锁成功的,所以在@synchronized(obj)时候,我们要保证obj没有被释放
当obj有值时候,我们先看一下SyncData的数据结构,
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;
因为mutex是一个递归锁,所以搭配objc_sync_nil就可以避免出现死锁的产生
我们再看一下id2data的实现
我们查看一下LOCK_FOR_OBJ的实现
// 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;
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
我们发现其实每个加锁的obj都对应一个SyncList,我们再看一下StripedMap的结构,
- 我们发现StripedMap其实是一个抽象类,并且里面有一个indexForPointer的存储函数,
- 而存储函数是通过哈希函数得到一个index,并且我们知道@synchronized(obj)可以全局使用,并且只要obj不为空都能进行加锁,
- 所以我们猜测StripedMap应该有个全局的哈希表来通过obj得到一个index,然后存储SyncList,而SyncList存储的都是一个个的节点SyncData
我们再看一下id2data源码
static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS //环境定义--针对单个线程
// Check per-thread single-entry fast cache for matching object
bool fastCacheOccupied = NO;
// 检查每线程单项快速缓存中是否有匹配的对象--
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: {//加锁,则对加锁次数+1,然后重新缓存到线程的单项快速缓存中
lockCount++;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
break;
}
case RELEASE://解锁,如果解锁,则对加锁次数-1,然后重新缓存到线程的单项快速缓存中
lockCount--;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
if (lockCount == 0) {//lockCount为0时候就从tls中删除解锁
// remove from fast cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE--记录的线程数去掉,threadCount是记录的线程的情况
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
#endif//针对所有线程
// 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;
}
}
// Thread cache didn't find anything.
// Walk in-use list looking for matching object
// Spinlock prevents multiple threads from creating multiple
// locks for the same new object.
// We could keep the nodes in some hash table if we find that there are
// more than 20 or so distinct locks active, but we don't do that now.
//加锁
lockp->lock();
{
/*
再次从缓存中取,看看有没有,避免有其他线程已经对obj加锁
如果找到了就对threadCount+1
如果找不到就在SyncData接一个data
如果不是加锁咋不管
如果是加锁,则将新的data的objc设置为object,并且将threadCount加1
如果一个SyncData都没有那么就创建一个,设置好数据后保存在map中
最后如果是新创建的,那么就会在tls中保存数据
*/
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
//线程+1
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
//如果已经有了SyncData但是没有对应的object
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 ) {//则将object配置在后面的SyncData中
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
// Allocate a new SyncData and add to list.
// XXX allocating memory with a global lock held is bad practice,
// might be worth releasing the lock, allocating, and searching again.
// But since we never free these guys we won't be stuck in allocation very often.
//如果一个SyncData都没有,那就创建一个
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.
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) {//如果不是从缓存中读取的,那么就将SyncData缓存到tls中
// 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;
}
总结:
- 1、当加锁时候,系统会从先从当前线程的tls中取出object对应的SyncData,如果取到了走到2,
- 如果取不到就从全局SyncCache中遍历寻找与object对应的SyncCacheItem,如果取到了就走到2,
- 如果取不到就走到3
- 2、取到SyncData或者SyncCacheItem,先取出对象中的lockCount,代表的是加锁的次数,
- 如果是加锁,则把lockCount加一,保存到线程的tls中,
- 如果是解锁,则把lockCount减一,再保存到线程的tls中,减完之后如果lockCount为0时候,则将tls中的lockCount置空,并且通过OSAtomicDecrement32Barrier将SyncData的线程数置空,然后将SyncData或者SyncCacheItem返回
- 3、假如都没找到,说明该对象以前没加锁过,为了保证不重复创建SyncData,下面这段是加锁状态,
- 先从listp中遍历SyncData,如果找到与object对应的SyncData(可能其他线程创建了),那么直接跳到4;
- 如果没找到object对应的SyncData,但是有空白的SyncData,则将空白的SyncData与object关联起来,并且将SyncData的threadCount设置为1,再跳到4;
- 假如没有空白的SyncData,则创建一个SyncData,然后与object关联起来将SyncData的threadCount设置为1,并且将创建的SyncData变为listp的第一个元素(listp是通过object通过哈希算法得到的index存储在在map表中,因为不同的object可能得到相同的index,所以此时listp是已经有数据了,为了不让数据丢失,所以会把数据赋值在取到数据的nextData中,类似遵循栈特征,先进的在后面)
- 4、准备好了SyncData,则解锁上面代码,如果是解锁,则不管,直接ruturn,如果是加锁,则将准备好的SyncData保存在当前线程tls的SYNC_DATA_DIRECT_KEY中,并将tls中的SYNC_COUNT_DIRECT_KEY设置为1,然后将SyncData返回;
- 因为synchronized需要不断对map表已经缓存进行读写操作,所以性能比较低
做一个验证:
-(void)testSynchronized{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self) {
NSLog(@"线程self---1加锁了");
[self logInfo];
sleep(3);
NSLog(@"线程self---1解锁了");
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(2);//保证线程2加锁在线程1后面
@synchronized (self) {
NSLog(@"线程self---2加锁");
[self logInfo];
NSLog(@"线程self---2解锁");
}
});
NSObject *objec = [NSObject new];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);//保证线程2加锁在线程1后面
@synchronized (objec) {
NSLog(@"线程objec---1加锁");
[self logInfo];
NSLog(@"线程objec---1解锁");
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);//保证线程2加锁在线程1后面
@synchronized (objec) {
NSLog(@"线程objec---2加锁了");
[self logInfo];
NSLog(@"线程objec2解锁了");
}
});
}
2020-03-05 21:09:49.299159+0800 003-NSLock分析[39805:6725003] 线程self---1加锁了
2020-03-05 21:09:49.299331+0800 003-NSLock分析[39805:6725003] 123
2020-03-05 21:09:50.301849+0800 003-NSLock分析[39805:6725002] 线程objec---1加锁
2020-03-05 21:09:50.302268+0800 003-NSLock分析[39805:6725002] 123
2020-03-05 21:09:50.302505+0800 003-NSLock分析[39805:6725002] 线程objec---1解锁
2020-03-05 21:09:50.302728+0800 003-NSLock分析[39805:6725011] 线程objec---2加锁了
2020-03-05 21:09:50.302925+0800 003-NSLock分析[39805:6725011] 123
2020-03-05 21:09:50.303114+0800 003-NSLock分析[39805:6725011] 线程objec2解锁了
2020-03-05 21:09:52.302250+0800 003-NSLock分析[39805:6725003] 线程self---1解锁了
2020-03-05 21:09:52.302565+0800 003-NSLock分析[39805:6725004] 线程self---2加锁
2020-03-05 21:09:52.302756+0800 003-NSLock分析[39805:6725004] 123
2020-03-05 21:09:52.302948+0800 003-NSLock分析[39805:6725004] 线程self---2解锁
我们发现对self和object加锁是互不影响的,只有对同一个对象加锁才有用,并且加锁的代码类似于加入了同步队列,依次执行
二、NSLock
@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 NS_AVAILABLE(10_5, 2_0);
@end
NSLock 遵循 NSLocking 协议,lock 方法是加锁,unlock 是解锁,tryLock 是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。
举个例子
//主线程中
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];
});
2016-08-19 14:23:09.659 ThreadLockControlDemo[1754:129663] 线程1
2016-08-19 14:23:11.663 ThreadLockControlDemo[1754:129663] 线程1解锁成功
2016-08-19 14:23:11.665 ThreadLockControlDemo[1754:129659] 线程2
线程 1 中的 lock 锁上了,所以线程 2 中的 lock 加锁失败,阻塞线程 2,但 2 s 后线程 1 中的 lock 解锁,线程 2 就立即加锁成功,执行线程 2 中的后续代码。
-(void)testLock{
NSLock *lock = [NSLock new];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
sleep(2);
NSLog(@"线程1");
[lock unlock];
NSLog(@"线程4");
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);
[lock lock];
NSLog(@"线程5");
[lock unlock];
NSLog(@"线程6");
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);
[lock tryLock];
NSLog(@"线程2");
[lock unlock];
NSLog(@"线程3");
});
}
2020-03-04 14:01:30.496134+0800 003-NSLock分析[37322:5093629] 线程2
2020-03-04 14:01:30.496493+0800 003-NSLock分析[37322:5093634] 线程5
2020-03-04 14:01:30.496490+0800 003-NSLock分析[37322:5093629] 线程3
2020-03-04 14:01:30.496758+0800 003-NSLock分析[37322:5093634] 线程6
2020-03-04 14:01:31.497772+0800 003-NSLock分析[37322:5093630] 线程1
2020-03-04 14:01:31.498132+0800 003-NSLock分析[37322:5093630] 线程4
由上面的结果可得知,当tryLock返回NO时候不会阻塞线程,意味着加锁失败,并且不管在哪个线程中执行unlock,那么其他线程堵塞也随之解除。
NSLock *lock = [[NSLock alloc] init];
for (int i= 0; i<100; i++) {
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-03-04 15:16:51.971619+0800 003-NSLock分析[46466:5215601] current value = 10
我们会发现当出现这种循环递归锁时候,会造成堵塞现象,因为第一次testMethod执行时候lock进行加锁了,然后第二次进来时候lock需要等待解锁后才能往下走,而lock需要等第一个testMethod执行完才能解锁,这就造成了互相等待的阻塞现象
三、NSConditionLock
@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 NS_AVAILABLE(10_5, 2_0);
@end
NSConditionLock 和 NSLock 类似,都遵循 NSLocking 协议,方法都类似,只是多了一个 condition 属性,以及每个操作都多了一个关于 condition 属性的方法,例如 tryLock,tryLockWhenCondition:,NSConditionLock 可以称为条件锁,只有 condition 参数与初始化时候的 condition 相等,lock 才能正确进行加锁操作。而 unlockWithCondition: 并不是当 Condition 符合条件时才解锁,而是解锁之后,修改 Condition 的值,这个结论可以从下面的例子中得出。
//主线程中
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lockWhenCondition:1];
NSLog(@"线程1");
sleep(2);
[lock unlock];
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保证让线程2的代码后执行
if ([lock tryLockWhenCondition:0]) {
NSLog(@"线程2");
[lock unlockWithCondition:2];
NSLog(@"线程2解锁成功");
} else {
NSLog(@"线程2尝试加锁失败");
}
});
//线程3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(2);//以保证让线程2的代码后执行
if ([lock tryLockWhenCondition:2]) {
NSLog(@"线程3");
[lock unlock];
NSLog(@"线程3解锁成功");
} else {
NSLog(@"线程3尝试加锁失败");
}
});
//线程4
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(3);//以保证让线程2的代码后执行
if ([lock tryLockWhenCondition:2]) {
NSLog(@"线程4");
[lock unlockWithCondition:1];
NSLog(@"线程4解锁成功");
} else {
NSLog(@"线程4尝试加锁失败");
}
});
2016-08-19 13:51:15.353 ThreadLockControlDemo[1614:110697] 线程2
2016-08-19 13:51:15.354 ThreadLockControlDemo[1614:110697] 线程2解锁成功
2016-08-19 13:51:16.353 ThreadLockControlDemo[1614:110689] 线程3
2016-08-19 13:51:16.353 ThreadLockControlDemo[1614:110689] 线程3解锁成功
2016-08-19 13:51:17.354 ThreadLockControlDemo[1614:110884] 线程4
2016-08-19 13:51:17.355 ThreadLockControlDemo[1614:110884] 线程4解锁成功
2016-08-19 13:51:17.355 ThreadLockControlDemo[1614:110884] 线程1
上面代码先输出了 ”线程 2“,因为线程 1 的加锁条件不满足,初始化时候的 condition 参数为 0,而加锁条件是 condition 为 1,所以加锁失败。locakWhenCondition 与 lock 方法类似,加锁失败会阻塞线程,所以线程 1 会被阻塞着,而 tryLockWhenCondition 方法就算条件不满足,也会返回 NO,不会阻塞当前线程。
回到上面的代码,线程 2 执行了 [lock unlockWithCondition:2]; 所以 Condition 被修改成了 2。
而线程 3 的加锁条件是 Condition 为 2, 所以线程 3 才能加锁成功,线程 3 执行了 [lock unlock]; 解锁成功且不改变 Condition 值。
线程 4 的条件也是 2,所以也加锁成功,解锁时将 Condition 改成 1。这个时候线程 1 终于可以加锁成功,解除了阻塞。
从上面可以得出,NSConditionLock 还可以实现任务之间的依赖。
四、NSRecursiveLock
@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
NSRecursiveLock 是递归锁,他和 NSLock 的区别在于,NSRecursiveLock 可以在一个线程中重复加锁(反正单线程内任务是按顺序执行的,不会出现资源竞争问题),NSRecursiveLock 会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功。
像上面NSLock时候在一个线程中重复加锁的话,会造成堵塞现象,我们可以吧NSLock改成NSRecursiveLock就可以了
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value:%d", value);
RecursiveBlock(value - 1);
}
[lock unlock];
};
RecursiveBlock(2);
});
2016-08-19 14:43:12.327 ThreadLockControlDemo[1878:145003] value:2
2016-08-19 14:43:12.327 ThreadLockControlDemo[1878:145003] value:1
五、NSCondition
@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
NSCondition 的对象实际上作为一个锁和一个线程检查器,锁上之后其它线程也能上锁,而之后可以根据条件决定是否继续运行线程,即线程是否要进入 waiting 状态,经测试,NSCondition 并不会像上文的那些锁一样,先轮询,而是直接进入 waiting 状态,当其它线程中的该锁执行 signal 或者 broadcast 方法时,线程被唤醒,继续运行之后的方法。
-(void)testCondition{
NSCondition *lock = [[NSCondition alloc] init];
NSMutableArray *array = [[NSMutableArray alloc] init];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
if (!array.count) {
[lock wait];
}
[array removeAllObjects];
NSLog(@"array removeAllObjects");
[lock unlock];
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保证让线程2的代码后执行
[lock lock];
[array addObject:@1];
NSLog(@"array addObject:@1");
[lock signal];
NSLog(@"发送信号");
[lock unlock];
});
}
2020-03-04 18:30:07.484889+0800 003-NSLock分析[54265:5454159] array addObject:@1
2020-03-04 18:30:07.485264+0800 003-NSLock分析[54265:5454159] 发送信号
2020-03-04 18:30:07.485530+0800 003-NSLock分析[54265:5454160] array removeAllObjects
NSCondition锁可以对多个线程同时进行加锁,并且在一个线程加锁后可以通过wait让线程暂停下来,只有其他线程发送了signal或者broadcast信号才能继续往下执行,其中 signal 和 broadcast 方法的区别在于,signal 只是一个信号量,只能唤醒一个等待的线程,想唤醒多个就得多次调用,而 broadcast 可以唤醒所有在等待的线程。如果没有等待的线程,这两个方法都没有作用。
六、锁的分类
1、自旋锁 - atomic
线程反复检查锁变量是否可用,由于线程在这一过程中保持执行,因此是一种忙等待,一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁,自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的,而atomic就是通过个set和get方法添加一个自旋锁,关于atomic和nonatomic看这篇文章atomic和nonatomic的分析
2、互斥锁
互斥锁是用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制,该目的通过将代码切成一个个的临界区而达成,就像假如一个线程在执行还没解锁,那当前线程就休息一会,自旋锁是盲等,意思就是这个线程没完成,那另一个线程只能在那等待,所以自旋转比互斥锁性能消耗高,但是效率是最高的
属于互斥锁的有
- NSLock
- pthread_mutex
- @synchronized
3、读写锁
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行访问,写者则需要对共享资源进行写操作,
这种锁对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同事多个读者俩访问共享资源,最大可能的读者数为实际的逻辑CPU数,写者是排他性的,一个读写锁同事只能有一个写者或多个读者,但不能同时既有读者又有写者,在读写锁保持期间也是抢占失效的。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,知道没有任何写者或读者,如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在哪里,知道写者释放该读写锁
一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁,正式因为这个特性
- 当读写锁是写加锁状态时,在这个锁呗解锁之前,所有视图对这个锁加锁的线程都会被堵塞
- 当对俄所在读加锁状态时,所有视图以读模式对她进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对次锁,它必须直到所有的线程释放锁
- 通常,当读写锁处于读模式锁住状态时,如果有另外线程试图以写模式加锁,读写锁通常会阻塞随后的读模式请求,这样可以避免读模式锁长期占用,而等待的写模式锁请求长期阻塞
- 读写锁适合对数据结构的读次数比写次数多得多的情况,因为,读模式锁定时可以共享,以写模式锁住时一位置独占,所以读写锁又叫共享-独占锁
实现:
#pragma mark - 读数据
- (id)lg_objectForKey:(NSString *)key{
__block id obj;
// 同步读取指定数据:
dispatch_sync(self.concurrent_queue, ^{
obj = [self.dataCenterDic objectForKey:key];
});
return obj;
}
#pragma mark - 写数据
- (void)lg_setObject:(id)obj forKey:(NSString *)key{
// 异步栅栏调用设置数据:
dispatch_barrier_async(self.concurrent_queue, ^{
[self.dataCenterDic setObject:obj forKey:key];
});
}
我们可以创建一个并发队列,对于读操作,我们可以用同步函数进行处理,对于写操作我们可以用异步栅栏函数这操作,这样就简单实现了一个读写锁