「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」
前言
多线程的学习系列,这是最后一篇了,通过了解八大锁,我们会知道如何去保护线程安全,保护数据的安全。自旋锁,互斥锁,条件锁,递归锁,读写锁等等,这里都会进行一一介绍。这里我先列一下前面写的文章:
锁的性能
锁的大类分为两种:互斥锁
和自旋锁
。
互斥锁
:互斥锁是用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。当前线程得到一个临界区的锁,而此时这个锁正被另外一个线程所持有,那么当前线程就会被阻塞,进入等待队列,处于休眠状态。
自旋锁
:线程反复检查锁变量是否可用,由于线程在这一过程中保持执行,因此是一种忙等待,一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁,自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
下图是各种锁的性能。
下面我就按这个顺序来介绍好了。
OSSpinLock
OSSpinLock 是自旋锁,性能是最高的,但是会一直轮询,等待时会消耗大量 CPU 资源。所以苹果已经推荐os_unfair_lock
来代替。
上面使用OSSpinLock
编译会报警告,在iOS10已经废弃了
,OSSpinLock
大家也就已经不再使用.
os_unfair_lock
是一个互斥锁,它不像 OSSpinLock 需要一直忙等。
那就来个售票的例子吧。
@property (nonatomic, assign) os_unfair_lock unfairLock;
先初始化 os_unfair_lock
。
_unfairLock = OS_UNFAIR_LOCK_INIT;
售票请求:
- (void)jj_test_os_unfair_lock{
self.ticketCount = 20;
for (int i = 0; i< 11; i++) {
dispatch_queue_t queue = dispatch_queue_create("jj.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
[self saleTicket];
});
}
for (int i = 0; i< 11; i++) {
dispatch_queue_t queue = dispatch_queue_create("jj2.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
[self saleTicket];
});
}
}
没有加锁的时候
- (void)saleTicket{
if (self.ticketCount > 0) {
self.ticketCount--;
NSLog(@"当前窗口:余票还剩:%lu张",(unsigned long)self.ticketCount);
}else{
NSLog(@"当前窗口:当前车票已售罄");
}
}
我们就看下没有加锁的时候会不会有问题。
看到没,是不是出问题了。
那如果我们加一下锁呢?
- (void)saleTicket{
os_unfair_lock_lock(&_unfairLock);
if (self.ticketCount > 0) {
self.ticketCount--;
NSLog(@"当前窗口:余票还剩:%lu张",(unsigned long)self.ticketCount);
}else{
NSLog(@"当前窗口:当前车票已售罄");
}
os_unfair_lock_unlock(&_unfairLock);
}
来,我们看下效果。
简单分析一波:我们第一个线程进来的时候先加锁,然后开始对票进行数据处理。第二个线程进来的时候,发现被锁了,就开始休眠等待,等数据减1后了,unlock 了,这时候第二个线程就被唤醒,就进来了,同时加锁处理。这样就能保证数据的安全。这就是互斥锁。
dispatch_semaphore
dispatch_semaphore
用来初始化信号量,通过信号量的值可以控制线程哪个执行,哪个需要等待。并且设置 GCD 的最大并发数。值为 1 的时候,还能达到同步锁
的效果。
-
dispatch_semaphore_create
:创建一个Semaphore
并初始化信号的总量。 -
dispatch_semaphore_signal
:发送一个信号,让信号总量加 1。 -
dispatch_semaphore_wait
:如果信号量大于 0,则正常执行,而且信号量会减 1 ;如果信号量为 0 ,则会一直等待,等接收到通知信号量大于 0 后才可以正常执行。如果等待的时候会起到阻塞当前线程的效果。
我们来个例子看下如果实现同步线程的效果:
- (void)dispatch_semaphore_t_request2
{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_queue_create("jj.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"1--%@",[NSThread currentThread]);
sleep(2);
NSLog(@"1-完成");
dispatch_semaphore_signal(sema);
});
dispatch_async(queue, ^{
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"2--%@",[NSThread currentThread]);
sleep(1);
NSLog(@"2-完成");
});
NSLog(@"main--%@",[NSThread currentThread]);
}
我们可以看到,线程 6 先执行,而且是等任务 1 完成后,线程 7 才开始执行。这就是同步线程锁的作用。异步串行队列的任务都是在一个线程内执行的。这里是不同线程也实现同步线程执行。
pthread_mutex
pthread_mutex 也是一个互斥锁。NSLock 的底层封装就是用 pthread_mutex 。
那我们继续用上面买票的例子。
@property (nonatomic, assign) pthread_mutex_t mutex_t;
初始化锁
pthread_mutex_init(&_mutex_t, NULL);
售票:
- (void)saleTicket{
pthread_mutex_lock(&_mutex_t);
if (self.ticketCount > 0) {
self.ticketCount--;
NSLog(@"当前窗口:余票还剩:%lu张",(unsigned long)self.ticketCount);
}else{
NSLog(@"当前窗口:当前车票已售罄");
}
pthread_mutex_unlock(&_mutex_t);
}
结果也是正常的,这就不贴出来了,和上面的os_unfair_lock
加锁效果是一样的。
NSLock
NSLock 也是一个互斥锁,底层就是用上面的 pthread_mutex 实现的。
它的使用方法就用 的协议。这里 NSLock 还有其它锁也遵循这个协议。
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
那这里的例子其实和上面售票是一样的,也是直接用 lock 和 unlock 方法进行加锁处理就行。
NSCondition
NSCondition 是一个条件锁,也是遵循 NSLocking 协议。作为一个锁和一个线程检查器,锁上之后其它线程也能上锁,而之后可以根据条件决定是否继续运行线程。
- wait:会阻塞线程,使其进入休眠状态,直至超时。
- waitUntilDate:会阻塞线程,使其进入休眠状态,直至某个 Date 时刻。
- signal:会唤醒一个正在休眠等待的线程。
- broadcast:会唤醒所有正在等待的线程。
我们先来一个生产消费者的例子:
- (void)jj_testConditon {
_testCondition = [[NSCondition alloc] init];
//创建生产-消费者
for (int i = 0; i < 20; i++ {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self jj_producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self jj_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self jj_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self jj_producer];
});
}
我们给生产者加锁。保证添加数据安全。
- (void)jj_producer{
[_testCondition lock];
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
[_testCondition unlock];
}
我们同时也给消费者加锁,保证数据安全。如果数据为0,我们就while,知道它不为0。
- (void)jj_consumer{
while (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
}
[_testCondition lock];
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
[_testCondition unlock];
}
你们觉得这么做,有没有问题,我们看下打印结果:
结果发现,数据是不是还是有问题。
所以,我们这里需要添加一下 wait ,和 signal ,来保证条件的安全。
每生产一个,我们都发送一个 signal 来告知,有数据了,你可以消费数据了。
- (void)jj_producer{
[_testCondition lock];
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
[_testCondition signal];
[_testCondition unlock];
}
我们在消费者那里,如果为 0 ,我们就等待 wait ,等生产发送 signal ,我们就可以直接 -1 操作了。
- (void)jj_consumer{
[_testCondition lock];
// 线程安全
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
// 保证正常流程
[_testCondition wait];
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
[_testCondition unlock];
}
看下打印效果:
NSRecursiveLock
NSRecursiveLock:递归锁,是一种特殊的互斥锁,他和 NSLock 的区别在于,NSRecursiveLock 可以在一个线程中重复加锁,它会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁。也是遵循 NSLocking 协议。
- (void)jj_testRecursive {
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
[lock lock];
NSLog(@"current value = %d",value);
testMethod(value - 1);
[lock lock];
}
};
testMethod(10);
}
NSConditionLock
NSConditionLock
:也是一个条件锁,一旦一个线程获得锁,其他线程一定等待。其本质就是 NSCondition + Lock
。
-
initWithCondition:初始化条件。
-
lockWhenCondition:加锁,满足条件,就执行,不是这个条件,阻塞线程,好好等待。
-
unlockWithCondition:解锁,更改条件。
举个简单例子来说明一下:
- (void)jj_testConditonLock{
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1];
NSLog(@"线程 1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2];
NSLog(@"线程 2");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"线程 3");
[conditionLock unlock];
});
}
先分析一波:添加一个条件锁,条件初始化为 2 ,然后是3 个异步并发。
其中线程 1 的队列优先级最高,进来就加锁,判断条件是否为 1 ,很明显不是 1 ,所以就等待。
线程 2 进来,发现条件刚好是 2 ,加锁,条件是 2 ,所以会比线程 1 先执行。
线程 3 进来,也是可以做加锁,减锁的操作,看那个先执行加锁操作。
结果果然是这样,不管我运行多少次,线程 2 都要比线程 1 先执行。
@synchronized
@synchronized
是一个互斥递归锁,虽然性能最低,但是简单易用,容易上手。
这的互斥锁,我就不用上面的售票的例子,也是可以实现一样的效果的。
我们换一个例子:
看一下线程的信息:
明显就是多次初始化的时候,在先 release 后retain 多次操作的情况下,出现对象还没有 retain 完,还进行 release 操作导致出错的。
那这里有 2 个方案。 1 个就是使用 atomic 修饰我们的数组。在iOS 10之前 automic
是用的自旋锁, iOS 10 之后使用的是 os_unfair_lock
。也就是在 set 和 get 方法里面,会进行加锁处理。一般我们都不使用。
另外一种方案就是我们的 @synchronized。
- (void)jj_crash{
for (int i = 0; i < 5000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self) {
self.testArray = [NSMutableArray array];
}
});
}
}
最后
八大锁已经是介绍完毕了,至于使用那种锁是没有绝对,可灵活运用,如果是读写锁,可以用我们上一篇的提到的栅栏函数进行处理。