讲到锁这个话题,开头先推荐大佬的博客 不再安全的 OSSpinLock
一:基础介绍
什么是互斥锁
互斥锁:顾名思义,互相排斥。线程A获取到锁,在释放锁之前,其他线程都获取不到锁。互斥锁也分为两种:
递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用非递归锁:不可重入,必须等锁释放后才能再次获取锁
什么是自旋锁
自旋锁:线程A获取到锁,在释放锁之前,线程B又来获取锁,此时获取不到,线程B就会不断的进入循环,一直检查锁是否已被释放,处于忙等状态。如果释放,则能获取到锁
自旋锁和互斥锁的特点
-
自旋锁会忙等,所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。 -
互斥锁会休眠,所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作,直到被锁资源释放锁。此时会唤醒休眠线程。 -
自旋锁优缺点
优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用。
锁的性能比较
性能方面,常见的锁性能如下:
锁的作用
在编程中,特别是多线程开发者中,来保证共享数据操作的完整性。假如有 ABC三条甚至更多的线程,同时去访问资源,那么读的话是没有问题,要是写的话,就可能出问题,同时修改了某一个数据,这样就破坏的数据的完整性了。
而加锁的话,就是同一个时间,只能有一个个线程访问,其他的靠边等待,可以给每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
卖票示例,A,B两个窗口卖票
不加锁的情况
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketCount = 20;
[self saleTicket];
}
- (void)saleTicket{
// A窗口
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10; i++) {
[self detailSaleTicket];
}
});
// B窗口
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10; i++) {
[self detailSaleTicket];
}
});
}
- (void)detailSaleTicket{
// @synchronized (self) {
// }
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%ld张",self.ticketCount);
}else{
NSLog(@"当前车票已售罄");
}
}
打印结果:
**2021-09-03 09:47:10.241853+0800 001-@synchronized分析[56982:8372629] 当前余票还剩:18张**
**2021-09-03 09:47:10.241856+0800 001-@synchronized分析[56982:8372618] 当前余票还剩:18张**
**2021-09-03 09:47:10.241911+0800 001-@synchronized分析[56982:8372629] 当前余票还剩:17张**
**2021-09-03 09:47:10.241994+0800 001-@synchronized分析[56982:8372618] 当前余票还剩:16张**
**2021-09-03 09:47:10.242322+0800 001-@synchronized分析[56982:8372629] 当前余票还剩:15张**
**2021-09-03 09:47:10.242388+0800 001-@synchronized分析[56982:8372629] 当前余票还剩:13张**
**2021-09-03 09:47:10.242325+0800 001-@synchronized分析[56982:8372618] 当前余票还剩:14张**
**2021-09-03 09:47:10.242491+0800 001-@synchronized分析[56982:8372629] 当前余票还剩:12张**
**2021-09-03 09:47:10.242550+0800 001-@synchronized分析[56982:8372618] 当前余票还剩:11张**
**2021-09-03 09:47:10.242583+0800 001-@synchronized分析[56982:8372629] 当前余票还剩:10张**
**2021-09-03 09:47:10.242663+0800 001-@synchronized分析[56982:8372618] 当前余票还剩:9张**
......
从打印结果来看,肯定是有问题的,出现了票数混乱
加锁的情况
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketCount = 20;
[self saleTicket];
}
- (void)saleTicket{
// A窗口
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10; i++) {
[self detailSaleTicket];
}
});
// B窗口
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10; i++) {
[self detailSaleTicket];
}
});
}
- (void)detailSaleTicket{
@synchronized (self) {
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%ld张",self.ticketCount);
}else{
NSLog(@"当前车票已售罄");
}
}
}
打印结果:
**2021-09-03 10:09:24.989503+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:19张**
**2021-09-03 10:09:24.989558+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:18张**
**2021-09-03 10:09:24.989624+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:17张**
**2021-09-03 10:09:24.989658+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:16张**
**2021-09-03 10:09:24.989708+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:15张**
**2021-09-03 10:09:24.989908+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:14张**
**2021-09-03 10:09:24.989961+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:13张**
**2021-09-03 10:09:24.989996+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:12张**
**2021-09-03 10:09:24.990029+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:11张**
**2021-09-03 10:09:24.990061+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:10张**
**2021-09-03 10:09:24.990127+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:9张**
**2021-09-03 10:09:24.990222+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:8张**
**2021-09-03 10:09:24.990289+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:7张**
**2021-09-03 10:09:24.990339+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:6张**
**2021-09-03 10:09:24.990416+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:5张**
**2021-09-03 10:09:24.990503+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:4张**
**2021-09-03 10:09:24.993305+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:3张**
**2021-09-03 10:09:24.993433+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:2张**
**2021-09-03 10:09:24.993556+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:1张**
**2021-09-03 10:09:24.993617+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:0张**
从结果来看,加锁之后票按次减少,线程安全。
二: 自旋锁
1. OSSpinLock
自从OSSpinLock出现了安全问题之后就废弃了。自旋锁之所以不安全,是因为自旋锁由于获取锁时,线程会一直处于忙等待状态,造成了任务的优先级反转
而OSSpinLock忙等的机制就可能造成高优先级一直running等待,占用CPU时间片;而低优先级任务无法抢占时间片,变成迟迟完不成,不释放锁的情况。
2. atomic
-
atomic: 是原子属性,是为多线程开发准备的,是默认属性!仅仅在属性的
setter方法中,增加了锁(自旋锁),能够保证同一时间,只有一条线程对属性进行写操作,同一时间 单(线程)写多(线程)读的线程处理技术。 -
nonatomic: 是非原子属性,没有加锁操作!性能高!
源码分析
setter方法,最后都会统一调用reallySetProperty方法
看下对是否存在atomic的判断:
- 原子性修饰的属性进行了
spinlock加锁处理,此处spinlock内部实际用os_unfair_lock替代了OSSpinLock。 - 非原子性的属性除了没加锁,其他逻辑与
atomic一般无二
getter和setter相似
因此atomic只能保证setter、getter方法的线程安全,并不能保证数据安全,我们来看个🌰
self.index = 0;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (NSInteger i = 0; i < 10000; i++) {
self.index = self.index + 1;
NSLog(@"%ld === %ld",(long)i,(long)self.index);
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (NSInteger i = 0; i < 10000; i++) {
self.index = self.index + 1;
NSLog(@"%ld === %ld",(long)i,(long)self.index);
}
});
理想情况下,最终index应该是20000,但最终结果却不是。
atomic保证变量在取值和赋值时的线程安全- 但不能保证
self.index+1也是安全的 - 如果改成
self.index=i是能保证setter方法的线程安全的
3. 读写锁
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的CPU数。
-
写者是排他性的,⼀个读写锁同时只能有⼀个写者或多个读者(与CPU数相关),但不能同时既有读者⼜有写者。在读写锁保持期间也是抢占失效的
-
如果读写锁当前没有读者,也没有写者,那么写者可以⽴刻获得读写锁,否则它必须⾃旋在那⾥,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以⽴即获得该读写锁,否则读者必须⾃旋在那⾥,直到写者释放该读写锁。
-
pthread_rwlock_t lock;// 结构 -
pthread_rwlock_init(&lock, null);// 初始化 -
pthread_rwlock_rdlock(&lock);// 读加锁 -
pthread_rwlock_tryrdlock(&lock);// 读尝试加锁 -
pthread_rwlock_wdlock(&lock);// 写加锁 -
pthread_rwlock_trywdlock(&lock);// 写尝试加锁 -
pthread_rwlock_unlock(&lock);// 解锁 -
pthread_rwlock_destory(&lock);// 销毁
下面我们使用pthread_rwlock_t实现多读单写功能:
从结果来看,写的时候都说单独存在,读的时候可以多个线程读取。
当然我们也可以使用GCD栅栏函数dispatch_barrier_async来实现
三: 互斥锁
1. @synchronized
@synchronized用法简单,但是需要注意的是它的性能是最差的。
@synchronized (self) {
// 🌹要执行的代码
}
上文中卖票示例加了@synchronized卖票就正常了,那么问题来了,为什么加了一把@synchronized锁之后,数据就安全了呢?为什么传入的参数是 self 呢?传入 nil 行不行呢?
定位源码
我们先cd到ViewController所在文件夹,使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o ViewController.cpp 命令生成.cpp文件`
可以看到,调用了objc_sync_enter方法,并且使用了try-catch,在正常处理流程中,提供了_SYNC_EXIT结构体,最后也会调用对应的析构函数objc_sync_exit。这里最重要的其实就是如下两个方法
objc_sync_enterobjc_sync_exit
如下图位置打上断点,运行起来看汇编代码
通过汇编,我们发现调用了objc_sync_enter和objc_sync_exit
这也验证了底层确实调用了objc_sync_enter和objc_sync_exit。
知道调用了什么方法依然无法解决我们的困惑,objc_sync_enter和objc_sync_exit做了些什么呢?
添加符号断点,最终定位到libobjc.A.dylib
相关数据结构
SyncData
typedef struct SyncData {
struct SyncData* nextData; //这个是一个单链表结构,其中包含了一个相同的数据结构。
object; //这里是使用了`DisguisedPtr`进行了包装,方便计算和传递。
threadCount; //线程的数量(多线程的时候使用),有多少个线程对该对象进行加锁的操作。
recursive_mutex_t mutex; //递归互斥锁。
} SyncData;
该数据结构为 @synchronized实现原理中最基本的数据结构,其中记录了提供的用于加锁的变量,使用该变量加锁的线程数以及与该变量一一对应的一个锁。
SyncCacheItem
typedef struct {
SyncData *data; //该缓存条目对应的SyncData
unsigned int lockCount; //该对象在该线程中被加锁的次数
} SyncCacheItem;
该数据结构用来记录某个 SyncData在某个线程中被加锁的记录,由定义可知,一个 SyncData可以被多个 SyncCacheItem持有。
SyncCache
typedef struct SyncCache {
unsigned int allocated; //该缓存此时对应的缓存大小
unsigned int used; //该缓存此时对应的已使用缓存大小
SyncCacheItem list[0]; //SyncCacheItem数组
} SyncCache;
该数据结构用来记录某个线程中所有 1SyncCacheItem1,并且记录了缓存大小以及已使用缓存大小。
StripedMap<SyncList>
struct SyncList {
SyncData *data; //SyncData数组
spinlock_t lock; //自旋锁
SyncList() : data(nil) { }
};
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
sDataLists是 StripedMap<SyncList>类型的一个静态变量。其中 StripedMap为一个最大可以存储64个变量的字典,LOCK_FOR_OBJ(obj)和 LIST_FOR_OBJ(obj)两个宏可以根据 obj的内存地址来获取对应的 SyncList中的 data和 lock。
_objc_pthread_data
在iOS中,每个线程都维护一个 _objc_pthread_data的结构体,该结构体下维护一个 SyncCache,该 SyncCache初始大小为 4个 SyncData大小,当 SyncCache缓存填满时,会以上次大小的 2倍进行扩充。
TLS
TLS全称为 Thread Local Storage,在iOS中,每个线程都拥有自己的 TLS,负责保存本线程的一些变量, TLS无需锁保护。
tls_get_direct/ tls_set_direct提供了快速从当前线程获取/设置对应变量的方法。
iOS中内设了两个宏,SYNC_DATA_DIRECT_KEY/ SYNC_COUNT_DIRECT_KEY,它们的用是与tsl_get_direct/ tls_set_direct配合,分别对 SyncCacheItem.data和 SyncCacheItem.lockCount进行读取与设置。
另外, _objc_pthread_data其实也是保存在 tls中的,它对应的读取关键字为 _objc_pthread_key。
源码分析
objc_sync_enter
根据官方的注释,objc_sync_enter内部使用了递归互斥锁。
objc_sync_exit
- 无论是
objc_sync_enter还是objc_sync_exit有一个共同操作,都会对@synchronized()传入的objc进行判断,如果objc为空,则什么都不会做。 objc_sync_enter和objc_sync_exit遥相呼应,一个加锁,一个解锁。- 当
objc不为空,在objc_sync_enter中,通过id2data方法获取一个SyncData类型的data,并对调用mutex属性进行上锁lock()操作。在objc_sync_exit中,同样获取对应的SyncData对象,然后调用data->mutex.tryUnlock()进行解锁。 SyncData是一个结构体(上面已经介绍)。
alignas(CacheLineSize)上层是一个StripedMap,前面已经介绍。
多线程下整体结构如下,其中SyncList就是一条线程
因此,从上述信息来看,@synchronized支持递归锁,并且支持多线程访问。
在objc_sync_enter中,通过id2data获取到的data,先看下它的源码实现,代码相对较长,我们折叠起来看下整体,然后再展开看细节。
主要分四个部分:
- 从线程缓存中获取当前线程的
SyncData。 - 从缓存中获取
SyncData。 - 缓存中没有找到的情况,内部处理。
- 进行缓存。
先看第一部分,从线程缓存中获取当前线程的SyncData,展开代码来看
再看第二部分,从缓存中获取SyncData
再看第三部分,当缓存中都找不到时候,内部处理
最后第四部分,进行缓存
-
@synchronized底层封装了recursive_mutex_t,之所以能够可重入,在于拓展了这个递归锁,增加了lockCount,为了防止多线程重入,又增加了threadCount进行处理,而这两点也是@synchronized的关键核心所在。 -
@synchronized之所以性能低,原因在于链表的查询,下层缓存的不断查找。
使用注意
我们先看下面一段代码
-(void)test {
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
_testArray = [NSMutableArray array];
});
}
}
这段代码运行起来肯定会崩,原因在于testArray不断的初始化,会调用setter方法,对新值retain,对旧值release,然而在这个过程中,会出现上一个还未release,下一个已经准备release,这样会导致野指针的产生。
我们使用@synchronized来处理。
-(void)test {
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (_testArray) {
_testArray = [NSMutableArray array];
}
});
}
}
然而......还是崩了,因为使用_testArray并没有实际意思,在释放的过程中某一个时刻_testArray是nil,那相当于没有锁,自然还是崩溃。
我们把_testArray换成self试试
-(void)test {
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self) {
_testArray = [NSMutableArray array];
}
});
}
}
完美解决问题,这里有个疑问,为什么使用self就可以了呢?原因在于self拥有长生命周期,并且self持有testArray,传入的对象self不会为nil。
总结
-
@synchronized在底层封装的是一把递归锁,所以这个锁是递归互斥锁 -
synchronized哈希表 - 拉链法 存储SyncData。sDataLists里面是一个array存储的是SyncList,SyncList里面是绑定的object,objc_sync_enter / exit对称 递归锁。两种存储 :TLS/Cache。第⼀次的时候SyncData才用头插法 -链表 ,标记thracount = 1, 然后下次再进来会判断是不是同⼀个对象 ,是同一个对象TLS --> lockCount ++,不是同一个的话TLS找不到 就会去创建一个SyncData则threadCount ++。objc_sync_exit的话就是lockCount--和threadCount-- -
@synchronized的可重入,即可嵌套,主要是由于lockCount和threadCount的搭配 -
@synchronized使用链表的原因是链表方便下一个data的插入, -
但是由于底层中
链表查询、缓存的查找以及递归,是非常耗内存以及性能的,导致性能低,所以在前文中,该锁的排名在最后 -
但是目前该锁的使用频率仍然很高,主要是因为
方便简单,且不用解锁 -
不能使用
非OC对象作为加锁对象,因为其object的参数为id -
@synchronized (self)这种适用于嵌套次数较少的场景。这里锁住的对象也并不永远是self,这里需要读者注意 -
如果锁嵌套次数较多,即
锁self过多,会导致底层的查找非常麻烦,因为其底层是链表进行查找,所以会相对比较麻烦,所以此时可以使用NSLock、信号量等
2. NSLock
先看上面的示例,使用NSLock同样能解决崩溃的问题
NSLock *jw_lock = [[NSLock alloc]init];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[jw_lock lock];
_testArray = [NSMutableArray array];
[jw_lock unlock];
});
}
源码分析
通过下符号断点的方式,定位到NSLock的源码在Foundation当中,但其并不开源,我们使用Swift版本swift-corelibs-foundation,来窥探OC下的实现逻辑。
从源码来看,实现其实比较简单,就是对pthread_mutex的封装使用。
使用注意
先看一段代码
for (int i= 0; i<100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
testMethod(value - 1);
}
};
testMethod(10);
});
}
打印结果:
看结果肯定是有问题的,我们使用NSLock来加锁,先看正确做法。
for (int i= 0; i<100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
[lock lock];
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
testMethod(10);
[lock unlock];
});
}
打印结果:
再来看错误的做法
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);
});
}
打印结果:
只打印了10,就什么都没有了,难道是卡死了吗?当然不是,这段代码是block内部出现了嵌套使用,互斥锁在递归调用时会造成堵塞,并非死锁——这里的问题是后面的代码无法执行下去。
- 第一次加完锁之后还没出锁就进行递归调用
- 第二次加锁就堵塞了线程(因为不会查询缓存)
解决:使用递归锁NSRecursiveLock替换NSLock
3. NSRecursiveLock
源码分析
NSRecursiveLock和NSLock源码类似,最重要的地方在上图标出,传入PTHREAD_MUTEX_RECURSIVE,标记它是递归锁。
for (int i= 0; i<100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
[recursiveLock lock];
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
[recursiveLock unlock];
};
testMethod(10);
});
}
打印结果:
4. NSCondition
NSCondition 的对象实际上作为⼀个锁和⼀个线程检查器:锁主要为了当检测条件时保护数据源,执⾏条件引发的任务;线程检查器主要是根据条件决定是否继续运⾏线程,即线程是否被阻塞。
[condition lock]: ⼀般⽤于多线程同时访问、修改同⼀个数据源,保证在同⼀时间内数据源只被访问、修改⼀次,其他线程的命令需要在lock 外等待,只到unlock,才可访问。[condition unlock]: 与lock 同时使⽤[condition wait]: 让当前线程处于等待状态[condition signal]:CPU发信号告诉线程不⽤在等待,可以继续执⾏
源码分析
底层也是封装了互斥锁。
使用
- (void)jw_testConditon{
_testCondition = [[NSCondition alloc] init];
//创建生产-消费者
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self jw_producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self jw_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self jw_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self jw_producer];
});
}
}
- (void)jw_producer{
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
}
- (void)jw_consumer{
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
}
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
}
查看打印结果的最后,这肯定是有问题的,读写混乱。可以看到出现了生产和消费对不上的情况,消费了一个还剩一个,再生产一个是2,却出现了3,这就是线程不安全访问的事故了。
所以我们要保证生产线、消费线数据的安全,就需要进行加锁处理,以保证多线程安全,但这只是它们内部的得到保证了,但是它们之间存在消费关系,比如生产的库存没有了,不得通知,消费者进行等待,生产好了再通知消费者来消费买单
下面我们使用NSCondition来解决这个问题
- (void)jw_testConditon{
_testCondition = [[NSCondition alloc] init];
//创建生产-消费者
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self jw_producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self jw_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self jw_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self jw_producer];
});
}
}
- (void)jw_producer{
[_testCondition lock]; 🌹// 操作的多线程影响
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
[_testCondition signal]; 🌹// 信号,生产了,可以消费
[_testCondition unlock]; 🌹
}
- (void)jw_consumer{
[_testCondition lock]; 🌹// 操作的多线程影响
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
[_testCondition wait];🌹// 数量为0 ,只能等待生产
}
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
[_testCondition unlock];🌹
}
使用NSCondition之后,打印的数据生产消费数量正确,解决了读写混乱的问题。
5. NSConditionLock
-
NSConditionLock是锁,⼀旦⼀个线程获得锁,其他线程⼀定等待 -
[xxxx lock]: 表示 xxx 期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition) 那它能执⾏此⾏以下代码,如果已经有其他线程获得锁(可能是条件锁,或者⽆条件锁),则等待,直⾄其他线程解锁 -
[xxx lockWhenCondition:A条件]: 表示如果没有其他线程获得该锁,但是该锁内部的condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且没有其他线程获得该锁,则进⼊代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直⾄它解锁。 -
[xxx unlockWithCondition:A条件: 表示释放锁,同时把内部的condition设置为A条件 -
return = [xxx lockWhenCondition:A条件 beforeDate:A时间]: 表示如果被锁定(没获得锁),并超过该时间则不再阻塞线程。但是注意:返回的值是NO,它没有改变锁的状态,这个函数的⽬的在于可以实现两种状态下的处理 -
所谓的
condition就是整数,内部通过整数⽐较条件
源码分析
使用
- (void)jw_testConditonLock{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"线程 1");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSLog(@"线程 2");
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"线程 3");
});
}
上面这段代码由于多线程的原因,打印肯定是无序的,下面我们使用NSConditionLock来控制打印顺序。
- (void)jw_testConditonLock{
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1];🌹// condition = 1 内部进行条件匹配,如果不相同就不执行,会往下走到线程2
NSLog(@"线程 1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2]; 🌹// 条件condition= 2 与外界条件相同,执行
NSLog(@"线程 2");
[conditionLock unlockWithCondition:1];🌹 // 执行完成 设置condition = 1
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock]; 🌹// 线程3之所以最先打印,是因为压根就没有条件,能正常执行,只是局部加减锁
NSLog(@"线程 3");
[conditionLock unlock];
});
}
打印结果一直是321。原因代码里已经标明。
四:参考
本篇章对多线程中锁进行了学习和记录,好记性不如烂笔头,以备不时之需!!!