前言
正文
一、锁的一些概念和性能对比
1.1 为什么要使用锁(线程安全)
线程安全是指,当一个线程访问数据的时候,其他的线程不能对其进行访问,直到该线程访问完毕。简单来讲就是在同一时刻,对同一个数据操作的线程只有一个。只有确保了这样,才能使数据不会被其他线程影响。而线程不安全,则是在同一时刻可以有多个线程对该数据进行访问,从而得不到预期的结果。
举例来说:现在仅剩余一张火车票,每一个购票请求都是一个线程,那么同一时刻有多个线程同时请求出票,那么剩余的这一张票将会同时出票多次,这明显是不合理的,所以锁的出现,就是为了确保线程安全问题。
1.2 锁的一些概念
-
临界资源: 多个线程共享各种资源,然而有很多资源一次只能供一个线程使用。一次仅允许一个线程使用的资源称为临界资源。
-
临界区:访问临界资源的代码区。
-
死锁:指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象(都要等待对方完成某个操作才能进行下一步),若无外力作用,它们都将无法推进下去,这时就会发生死锁。
-
上下文切换(Context Switch):在操作系统中,CPU切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态:当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。上下文切换包括保存当前任务的运行环境,恢复将要运行任务的运行环境。
-
轮询(Polling):一种CPU决策如何提供周边设备服务的方式,又称“程控输入输出”(Programmed I/O)。轮询法的概念是,由CPU定时发出询问,依序询问每一个周边设备是否需要其服务,有即给予服务,服务结束后再问下一个周边,接着不断周而复始。
-
原子属性:
-
atomic:原子属性,设置成员变量的@property属性时,默认为atomic,提供多线程安全。atomic是为setter方法加锁,将属性以atomic的形式来声明,该属性变量就能支持互斥锁了。而这种机制是耗费系统资源的。
-
nonatomic:非原子属性,不会为setter方法加锁,声明为该属性的变量,客户端应尽量避免多线程争夺同一资源。
-
-
自旋锁:线程反复检查锁变量是否可用(类似于
while(锁没解开)
),因此是一种忙等状态,将一直占用CPU资源。 -
读写锁:读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。
-
互斥锁:在被访问的资源被锁时,会让当前线程进入休眠状态,不再占用CPU资源,一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
-
条件锁:在一定条件下,让其等待休眠,并放开锁,等接收到信号或者广播,会从新唤起线程,并重新加锁。
-
递归锁:同一个线程可以多次加锁,不会造成死锁,不同线程来访问这段代码时,发现有锁要等待所有锁解开之后才可以继续往下走。
-
信号量(Semaphore):有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。
1.3 性能对比
锁的性能比较.png//10000000
OSSpinLock: 112.38 ms
dispatch_semaphore: 160.37 ms
os_unfair_lock: 208.87 ms
pthread_mutex: 302.07 ms
NSCondition: 320.11 ms
NSLock: 331.80 ms
pthread_rwlock: 360.81 ms
pthread_mutex(recursive): 512.17 ms
NSRecursiveLock: 667.55 ms
NSConditionLock: 999.91 ms
@synchronized: 1654.92 ms
//1000
OSSpinLock: 0.02 ms
dispatch_semaphore: 0.03 ms
os_unfair_lock: 0.04 ms
pthread_mutex: 0.06 ms
NSLock: 0.06 ms
pthread_rwlock: 0.07 ms
NSCondition: 0.07 ms
pthread_mutex(recursive): 0.09 ms
NSRecursiveLock: 0.12 ms
NSConditionLock: 0.18 ms
@synchronized: 0.33 ms
二、锁的使用(种类)
上锁有两种方式trylock
和lock
:当前线程锁失败,也可以继续其它任务,用 trylock
合适;当前线程只有锁成功后,才会做一些有意义的工作,那就 lock
,没必要轮询 trylock
。
注:以下大部分锁都会提供trylock
接口,不再作解释。
2.1 OSSpinLock (自旋锁)
- OSSpinLock 是一种自旋锁,也只有加锁,解锁,尝试加锁三个方法。
- OSSpinLock 会一直轮询,等待时会消耗大量 CPU 资源,不适用于较长时间的任务。
- OSSpinLock 有潜在的优先级反转问题,iOS10.0以后弃用了这种锁机制,使用os_unfair_lock。
需要导入头文件
#import <libkern/OSAtomic.h>
// 初始化
OSSpinLock spinLock = OS_SPINLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
OSSpinLockTry(&spinLock)
以GCD为例,代码以及执行结果如下:
2.1.1 OSSpinLockLock
// OSSpinLockLock
- (void)UseOSSpinLock{
__block OSSpinLock spinLock = OS_SPINLOCK_INIT;
dispatch_queue_t queue = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
OSSpinLockLock(&spinLock);
for(int i = 0; i < 3; i++){
sleep(1);
NSLog(@"线程1 第 %d 次",i);
}
OSSpinLockUnlock(&spinLock);
});
dispatch_async(queue, ^{
OSSpinLockLock(&spinLock);
for(int i = 0; i < 3; i++){
sleep(1);
NSLog(@"线程2 第 %d 次",i);
}
OSSpinLockUnlock(&spinLock);
});
dispatch_async(queue, ^{
OSSpinLockLock(&spinLock);
for(int i = 0; i < 3; i++){
sleep(1);
NSLog(@"线程3 第 %d 次",i);
}
OSSpinLockUnlock(&spinLock);
});
}
OSSpinLockLock
我们可以看到,我们用的并发异步线程,但是加锁之后,执行结果并没有并发异步执行。
2.1.2 OSSpinLockTry
// OSSpinLockTry
- (void)UseOSSpinLock{
__block OSSpinLock spinLock = OS_SPINLOCK_INIT;
dispatch_queue_t queue = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
OSSpinLockTry(&spinLock);
for(int i = 0; i < 3; i++){
sleep(1);
NSLog(@"线程4 第 %d 次",i);
}
OSSpinLockUnlock(&spinLock);
});
dispatch_async(queue, ^{
OSSpinLockTry(&spinLock);
for(int i = 0; i < 3; i++){
sleep(1);
NSLog(@"线程5 第 %d 次",i);
}
OSSpinLockUnlock(&spinLock);
});
dispatch_async(queue, ^{
OSSpinLockTry(&spinLock);
for(int i = 0; i < 3; i++){
sleep(1);
NSLog(@"线程6 第 %d 次",i);
}
OSSpinLockUnlock(&spinLock);
});
}
OSSpinLockTry-1
执行结果可以看出来,OSSpinLockTry并没有阻塞线程。也符合上面所说:当前线程锁失败,也可以继续其它任务。但是这只是测试一下,项目中不要这么写,因为这样没有意义,可以如下:
//OSSpinLockTry
- (void)UseOSSpinLock{
__block OSSpinLock spinLock = OS_SPINLOCK_INIT;
dispatch_queue_t queue = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
if(OSSpinLockTry(&spinLock)){
for(int i = 0; i < 3; i++){
sleep(1);
NSLog(@"线程7 第 %d 次",i);
}
OSSpinLockUnlock(&spinLock);
}else{
NSLog(@"锁7失败,执行一些其他事情");
}
});
dispatch_async(queue, ^{
if(OSSpinLockTry(&spinLock)){
for(int i = 0; i < 3; i++){
sleep(1);
NSLog(@"线程8 第 %d 次",i);
}
OSSpinLockUnlock(&spinLock);
}else{
NSLog(@"锁8失败,执行一些其他事情");
}
});
dispatch_async(queue, ^{
if(OSSpinLockTry(&spinLock)){
for(int i = 0; i < 3; i++){
sleep(1);
NSLog(@"线程9 第 %d 次",i);
}
OSSpinLockUnlock(&spinLock);
}else{
NSLog(@"锁9失败,执行一些其他事情");
}
});
}
OSSpinLockTry-2
执行结果可以看出,加锁失败后执行另一部分代码,并没有自旋去等待加锁,执行后其他锁释放也不会再次加锁,所以用的时候要考虑场景。
2.2 os_unfair_lock(互斥锁)
This is a replacement for the deprecated OSSpinLock. This function doesn't spin on contention, but instead waits in the kernel to be awoken by an unlock.
自我理解为:“这是对已弃用的osspinlock的替换。这个函数不会在争用时自旋,而是在内核中等待解锁来唤醒。”所以,它应该是互斥锁,并不是自旋锁。
需要导入头文件
#import <os/lock.h>
// 初始化
os_unfair_lock unfair_lock = OS_UNFAIR_LOCK_INIT;
// 加锁
os_unfair_lock_lock(&unfair_lock);
// 解锁
os_unfair_lock_unlock(&unfair_lock);
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
os_unfair_lock_trylock(&unfair_lock);
/*
注:解决不同优先级的线程申请锁的时候不会发生优先级反转问题.
不过相对于 OSSpinLock , os_unfair_lock性能方面减弱了许多.
*/
使用方法上同,不做示范。
2.3 dispatch_semaphore (信号量)
信号量,是持有计数的信号。个人觉得有点类似于引用计数:create
时定义最大线程数,使用时wait
进行计数-1,结束时signal
进行计数+1,当计数大于零时可执行,等于零时阻塞线程进行等待执行。
信号量的作用,个人觉得有以下几点:
- 控制线程数量(最大并发数),我们知道
NSOperationQueue
我们可以设置maxConcurrentOperationCount
来控制最大并发数,但是GCD的话,并没有一个属性可以控制最大并发数,所以我们可以用信号量来控制GCD的最大并发数。 - 线程安全(锁),信号量的本质还是控制子线程并发数量,而我们可以设置最大并发量为1,然后在临界区(多条线程都会访问的代码区)进行信号量控制,保证同一时刻只有一条线程执行此段代码,从而保证线程安全。
- 线程同步,会在后面代码说明。
- 阻塞线程,会在后面和同步代码一起说明。
// 初始化
dispatch_semaphore_t semaphore_t = dispatch_semaphore_create(1);
// 加锁
dispatch_semaphore_wait(semaphore_t,DISPATCH_TIME_FOREVER);
// 解锁
dispatch_semaphore_signal(semaphore_t);
2.3.1 最大并发数
不多说,直接上代码:
//2.dispatch_semaphore
- (void)UseSemaphore{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
for(int i = 0; i < 3; i++){
sleep(1);
NSLog(@"线程1 第%d次 线程:%@",i,[NSThread currentThread]);
}
dispatch_semaphore_signal(semaphore);
});
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
for(int i = 0; i < 3; i++){
sleep(1);
NSLog(@"线程2 第%d次 线程:%@",i,[NSThread currentThread]);
}
dispatch_semaphore_signal(semaphore);
});
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
for(int i = 0; i < 3; i++){
sleep(1);
NSLog(@"线程3 第%d次 线程:%@",i,[NSThread currentThread]);
}
dispatch_semaphore_signal(semaphore);
});
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
for(int i = 0; i < 3; i++){
sleep(1);
NSLog(@"线程4 第%d次 线程:%@",i,[NSThread currentThread]);
}
dispatch_semaphore_signal(semaphore);
});
}
最大并发数
通过结果我们可以看到,一共是四个并发异步线程,但是由于设置信号量,间接控制了最大并发数。值得注意的是:最大并发数,是指执行任务的线程最多是两个(信号量设置的是两个),但是,处于回收状态的线程不算此列.也就是说,执行任务的时候不只有两个线程,还有处于回收状态的线程,所以子线程个数不为2;
2.3.2 线程安全
线程不安全时:
- (void)UseSemaphoreLock{
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
self.number = 50;
__weak typeof(self) weakSelf = self;
dispatch_async(queue, ^{
for(int i = 0; i < 3; i++){
[weakSelf changeNumber];
}
});
dispatch_async(queue, ^{
for(int i = 0; i < 3; i++){
[weakSelf changeNumber];
}
});
}
- (void)changeNumber{
_number = _number - 1;
sleep(1);
NSLog(@"number == %ld",_number);
}
线程不安全时
我们可以看到,如果异步线程,同时更改同意资源时,那么可能出现数据混乱。所以我们可以用信号量加锁,保证线程安全:
- (void)UseSemaphoreLock{
self.semaphore1 = dispatch_semaphore_create(1);
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
self.number = 50;
__weak typeof(self) weakSelf = self;
dispatch_async(queue, ^{
for(int i = 0; i < 3; i++){
[weakSelf changeNumber];
}
});
dispatch_async(queue, ^{
for(int i = 0; i < 3; i++){
[weakSelf changeNumber];
}
});
}
- (void)changeNumber{
//相当于加锁
dispatch_semaphore_wait(_semaphore1, DISPATCH_TIME_FOREVER);
_number = _number - 1;
sleep(1);
NSLog(@"number == %ld",_number);
//相当于解锁
dispatch_semaphore_signal(_semaphore1);
}
线程安全时
如上,用信号量可以处理线程安全问题。(当然我们也可以把
wait
、signal
加到异步线程当中,但是觉得那么做的话,实际上还是控制了最大并发数,并不是解决线程安全。)
2.3.3 线程同步
//semaphore实现线程同步
- (void)semaphoreSync {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block int number = 0;
dispatch_async(queue, ^{
// 追加任务1
[NSThread sleepForTimeInterval:1]; // 模拟耗时操作
NSLog(@"任务1 %@",[NSThread currentThread]); // 打印当前线程
number = 100;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore---end1,number = %d",number);
dispatch_async(queue, ^{
// 追加任务2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"任务2 %@",[NSThread currentThread]); // 打印当前线程
number = 50;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore---end2,number = %d",number);
dispatch_async(queue, ^{
// 追加任务3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"任务3 %@",[NSThread currentThread]); // 打印当前线程
number = 10;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore---end3,number = %d",number);
}
同步
可以看到,虽然我们建立的并行异步线程,但是执行结果却是同步执行。原因如下:在主线程程中设置信号量为0
dispatch_semaphore_create(0)
;当执行到追加任务1的子线程时,进入子线程,主线程继续执行dispatch_semaphore_wait
,但是此时信号量为0,dispatch_semaphore_wait
在此处阻塞主线程进入等待状态,直到任务1的子线程执行dispatch_semaphore_signal
使信号量+1,此时主线程中处于等待的dispatch_semaphore_wait
可以使信号量-1,于是停止阻塞线程并继续向下执行。(利用阻塞线程实现线程同步)
2.4 pthread_mutex(互斥锁)
-
pthread_mutex
是 C 语言下多线程加互斥锁的方式。 - 被这个锁保护的临界区就只允许一个线程进入,其它线程如果没有获得锁权限,那就只能在外面等着。
需要导入头文件
#import <pthread/pthread.h>
//声明锁
pthread_mutex_t mutex_t;
// 初始化(两种)
1.普通初始化
pthread_mutex_init(&mutex_t, NULL);
2.宏初始化
pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;
// 加锁
pthread_mutex_lock(&mutex_t);
// 解锁
pthread_mutex_unlock(&mutex_t);
// 尝试加锁,可以加锁时返回的是 0,否则返回一个错误
pthread_mutex_trylock(& mutex_t)
// 释放锁
pthread_mutex_destroy(&_lock)
- 锁类型:
pthread_mutex_init(&mutex_t, NULL);
初始化锁 NULL等同于PTHREAD_MUTEX_DEFAULT
PTHREAD_MUTEX_NORMAL 缺省类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。
PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。
PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列
持续更新中..........