八大锁

1.概念

临界资源:

  • 在进程层面,一次仅允许一个进程使用的资源称为临界资源,如打印机等物理设备;
  • 在线程层面,多线程环境中一次仅允许一个线程使用的变量等就是临界资源;

临界区:

一个访问共享资源的程序代码片段就是临界区。当有线程进入临界区时,其他线程须保持等待。

锁:

是一种强行限制资源访问的同步机制,在并发控制中保证互斥的要求,用来防止多线程环境中对临界资源的脏读或者脏写。从某种角度而言,锁也可以看作是临界资源,线程获取到该锁对象之后才能执行里面的代码。

2.锁的分类

  • Mutex 互斥锁
  • Spin lock 自旋锁
  • Condition Variable 条件变量
  • Read/Write lock 读写锁

2.1.互斥锁

属于sleep-waiting类型,在申请上锁时如果锁已经被别的单元持有,则会让该线程睡眠,待锁释放时被唤醒。但线程的休眠和唤醒需要大量的CPU指令,因此需要花费一些时间。

2.2.自旋锁

属于busy-waiting类型,在申请上锁时如果锁已经被别的单元保持,并不是睡眠等待唤醒,而是循环检测保持者是否释放了锁。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。相对的,由于自旋锁一直占用CPU,所以如果长时间不能获得锁,则会降低CPU的使用效率。此锁比较适用于锁的持有者保存时间较短的情况下。

场景举例:

互斥锁:在某双核心的机器上有AB两个线程,分别运行在核心0核心1上。若线程A想要通过pthread_mutex_lock去得到一个临界区的锁,而这个锁此时正被线程B所持有,则线程A就会被阻塞,核心0会切换上下文,将线程A置于等待队列中不再占用核心0,此时核心0就可以运行其他的任务,例如另一个线程C的任务。

自旋锁:如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在核心0上忙等待并不停的请求锁,直到得到这个锁为止。

2.3.读写锁

高级别锁,区分读和写:

  • 一个读写锁允许多个线程同时读某共享资源;
  • 写操作具有排他性,写入数据时不允许其他线程对共享资源进行读或写。

读写锁适用于大量读少量写的环境,其效率相对普通的互斥锁和自旋锁要慢一个数量级。

3.八大锁

按照性能排序,从高到低:

  1. OSSpinLock
  2. dispatch_semaphore
  3. pthread_mutex
  4. NSLock
  5. NSCondition
  6. NSRecursiveLock
  7. NSConditionLock
  8. @synchronized

4.自旋锁

4.1.OSSpinLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//需要导入:#import <libkern/OSAtomic.h>
//OSSpinLock 自旋锁
- (void)OSSpinLock
{
__block OSSpinLock oslock = OS_SPINLOCK_INIT;
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"++++线程1 上锁");
OSSpinLockLock(&oslock);
sleep(4);
NSLog(@"++++线程1执行任务....");
OSSpinLockUnlock(&oslock);
NSLog(@"++++线程1 解锁");
});

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"++++线程2 上锁");
OSSpinLockLock(&oslock);
NSLog(@"++++线程2执行任务....");
OSSpinLockUnlock(&oslock);
NSLog(@"++++线程2 解锁");
});
}

输出日志:

1
2
3
4
5
6
++++线程1 上锁
++++线程2 上锁
++++线程2执行任务....
++++线程2 解锁
++++线程1执行任务....
++++线程1 解锁

ps:此自旋锁存在优先级反转问题,iOS10 之后苹果推出了os_unfair_lock来替代它。

关于优先级反转问题,百度百科 有更详细的介绍:

优先级翻转是当一个高优先级任务通过信号量机制访问共享资源时,该信号量已被一低优先级任务占有,因此造成高优先级任务被许多具有较低优先级任务阻塞,实时性难以得到保证。

例如:有优先级为A、B和C三个任务,优先级A>B>C,任务A,B处于挂起状态,等待某一事件发生,任务C正在运行,此时任务C开始使用某一共享资源S。在使用中,任务A等待事件到来,任务A转为就绪态,因为它比任务C优先级高,所以立即执行。当任务A要使用共享资源S时,由于其正在被任务C使用,因此任务A被挂起,任务C开始运行。如果此时任务B等待事件到来,则任务B转为就绪态。由于任务B优先级比任务C高,因此任务B开始运行,直到其运行完毕,任务C才开始运行。直到任务C释放共享资源S后,任务A才得以执行。在这种情况下,优先级发生了翻转,任务B先于任务A运行。

4.2.os_unfair_lock

OSSpinLock自旋锁存在优先级反转的问题,iOS10.0之后被废弃,由os_unfair_lock(不公平的锁)代替。

1
2
3
4
os_unfair_lock_t unfairLock;
unfairLock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(unfairLock);
os_unfair_lock_unlock(unfairLock);

5.互斥锁

5.1.pthread_mutex

pthread_mutex 是 C 语言下多线程加互斥锁的方式,需要 #import <pthread.h>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)pthread_mutex_Test
{
static pthread_mutex_t pLock;
pthread_mutex_init(&pLock, NULL);
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"++++线程1 上锁");
pthread_mutex_lock(&pLock);
sleep(3);
NSLog(@"++++线程1 执行任务....");
pthread_mutex_unlock(&pLock);
NSLog(@"++++线程1 解锁");
});

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"++++线程2 上锁");
pthread_mutex_lock(&pLock);
NSLog(@"++++线程2 执行任务....");
pthread_mutex_unlock(&pLock);
NSLog(@"++++线程2 解锁");
});
}

输出日志:

1
2
3
4
5
6
++++线程1 上锁
++++线程2 上锁
++++线程1 执行任务....
++++线程1 解锁
++++线程2 执行任务....
++++线程2 解锁

使用案例:开源图片下载和缓存框架PINRemoteImage

5.2.NSLock

NSLock 遵循 NSLocking 协议的互斥锁,通过lockunlock配合使用。

1
2
3
4
5
6
7
8
9
@interface NSLock : NSObject <NSLocking>
{
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name;
@end

互斥锁会使得线程阻塞,阻塞的过程又分两个阶段,第一阶段是会先空转,可以理解成跑一个 while 循环,不断地去申请加锁,在空转一定时间之后,线程会进入 waiting 状态,此时线程就不占用CPU资源了,等锁可用的时候,这个线程会立即被唤醒。

tryLock 则不会阻塞线程,如果获取锁成功则返回YES,获取失败则返回NO。这个方法无论如何都会立即返回,在拿不到锁时不会一直在那等待。

lockBeforeDate: 方法会在所指定 Date 之前尝试加锁,会阻塞线程。

ps: 如果使用 NSLock 连续锁定两次,则会造成死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- (void)nslockTest
{
NSLock *nslock = [NSLock new];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"++++线程1 上锁");
[nslock lock];
sleep(5);
NSLog(@"++++线程1执行任务....");
[nslock unlock];
NSLog(@"++++线程1 解锁");
});

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"++++线程2 上锁");
sleep(1);
BOOL succed = [nslock tryLock];
if (succed) {
NSLog(@"++++线程2上锁成功");
NSLog(@"++++线程2执行任务....");
[nslock unlock];
NSLog(@"++++线程2 解锁");
}else{
NSLog(@"++++线程2上锁失败");
}
});

//线程3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"++++线程3 上锁");
sleep(1);
[nslock lock];
NSLog(@"++++线程3执行任务....");
[nslock unlock];
NSLog(@"++++线程3 解锁");
});
}

输出日志:

1
2
3
4
5
6
7
8
++++线程2 上锁
++++线程3 上锁
++++线程1 上锁
++++线程2上锁失败
++++线程1执行任务....
++++线程1 解锁
++++线程3执行任务....
++++线程3 解锁

使用案例:AFNetworking

5.3.NSRecursiveLock

与NSLock不同,递归锁可以被一个线程多次获得,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。只有当所有的锁被释放之后,其他线程才可以获得锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)recursivelock
{
NSLock *lock = [NSLock new];
//NSRecursiveLock *lock = [NSRecursiveLock new];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

static void (^RecursiveBlock)(int);

RecursiveBlock = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"++++循环:%d", value);
sleep(1);
RecursiveBlock(value - 1);
}
[lock unlock];
};
RecursiveBlock(5);
});
}

上面就是一个死锁的情况,RecursiveBlock被递归的调用,从第二次开始,由于锁已经被使用且没有被解锁,所以需要等待解锁,造成死锁。运行后的日志就可以看出:

1
++++循环:5

这种情况下,如果把上片代码第二行注释取消,把NSLock替换为NSRecursiveLock,使用递归锁,则不会出现死锁。

1
2
3
4
5
++++循环:5
++++循环:4
++++循环:3
++++循环:2
++++循环:1

使用案例:PINRemoteImage

5.4.@synchronized

@synchronized(obj)的作用是根据给定对象,自动创建一个互斥锁,块中的代码执行完后才会释放锁。相比于使用NSLock等创建锁对象、加锁和解锁来说,@synchronized 用着更方便。但它也是这几个锁里效率最低的,因为一般我们会将self等作为代码块的加锁对象,这会造成其他共用此锁的同步块的阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@interface AppDelegate()
@property(nonatomic,strong) NSMutableArray *mArr;
@property(nonatomic,strong) NSMutableDictionary *mDic;
@end

@implementation AppDelegate

@synthesize mArr = _mArr;
@synthesize mDic = _mDic;

- (NSMutableArray *)mArr{
@synchronized (self) {
if (!_mArr) {
_mArr = [NSMutableArray array];
}
return _mArr;
}
}
- (void)setMArr:(NSMutableArray *)mArr{
@synchronized (self) {
_mArr = mArr;
NSLog(@"+++mArr updated~");
sleep(5);//模拟延时
NSLog(@"+++mArr block released~");
}
}
-(NSMutableDictionary *)mDic{
@synchronized (self) {
if (!_mDic) {
_mDic = [NSMutableDictionary dictionary];
}
return _mDic;
}
}
-(void)setMDic:(NSMutableDictionary *)mDic{
@synchronized (self) {
_mDic = mDic;
NSLog(@"+++mDic updated~");
}
}

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSLog(@"+++111");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"+++222");
self.mArr = [NSMutableArray arrayWithObject:@(0)];
self.mDic = [NSMutableDictionary dictionaryWithObject:@"v1" forKey:@"k"];
NSLog(@"+++333");
});

return YES;
}

输出日志:

1
2
3
4
5
6
+++111
+++222
+++mArr updated~
+++mArr block released~
+++mDic updated~
+++333

本示例中,数组属性mArr的存取器中都使用了@synchronized(self)来加锁,保证了数组读写的原子性,但字典属性mDic的存取函数中也使用了@synchronized(self),且加锁对象也是self,这就会导致在访问mArr的同时访问mDic时后者会被阻塞,造成没必要的等待。实际上,这种方式的效果与属性修饰符atomic一样,都存在效率的问题~

6.条件锁

6.1.NSCondition

NSCondition 扮演着两个角色:

A condition object acts as both a lock and a checkpoint in a given thread. The lock protects your code while it tests the condition and performs the task triggered by the condition. The checkpoint behavior requires that the condition be true before the thread proceeds with its task. While the condition is not true, the thread blocks. It remains blocked until another thread signals the condition object.

角色1:锁

NSCondition 实现了NSLocking协议,与NSLock一样可用来处理线程同步问题。

角色2:检查点

检查当前线程是否满足某个条件:条件不满足时线程会阻塞,直到另一个线程向该条件发送通知。

1
2
3
4
5
6
7
8
9
@interface NSCondition : NSObject <NSLocking> {

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

@property (nullable, copy) NSString *name;
@end

NSCondition 提供了 wait 和 signal 接口,接口命名与信号量类似。

场景:开启两个线程,线程A下载图片,线程B对图片做解码操作。

  • 线程B的要求是:如果没有图片则自动阻塞;
  • 当线程A下载完成后,则满足了线程B的需求,发送信号给B线程让其继续处理图片。

这样的场景就是生产者消费者模式(收发同步问题)。

原理:

  • 消费者取得锁,取产品,如果没有,则wait,直到有线程唤醒它去消费产品;
  • 生产者制造产品,首先也要取得锁,然后生产,再发signal,唤醒wait的消费者。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)nsConditonTest
{
NSCondition *condition = [[NSCondition alloc] init];
NSMutableArray *products = [NSMutableArray array];
// 线程1,消费者
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
[condition lock];
if ([products count] == 0) {
NSLog(@"+++等待商品..");
[condition wait]; //不满足条件 阻塞线程
}
[products removeObjectAtIndex:0];
NSLog(@"+++成功消费1个商品");
[condition unlock];
}
});
// 线程2,生产者
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
[condition lock];
[products addObject:[[NSObject alloc] init]];
NSLog(@"+++生产一个商品,现总量:%zi",products.count);
[condition signal]; //满足条件,通知其他线程
[condition unlock];
sleep(1);
}
});
}

输出日志:

1
2
3
4
5
6
+++等待商品..
+++生产一个商品,现总量:1
+++成功消费1个商品
+++等待商品..
+++生产一个商品,现总量:1
+++成功消费1个商品

使用案例:PINRemoteImage

6.2.NSConditionLock

相比于 NSLock 多了个 condition 参数,可以理解为一个条件标识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (void)conditionlock{
//初始锁对象 条件=0
NSConditionLock *contidionlock = [[NSConditionLock alloc] initWithCondition:0];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if([contidionlock tryLockWhenCondition:0]){
NSLog(@"++++执行线程1");
[contidionlock unlockWithCondition:1];
}else{
NSLog(@"++++上锁失败");
}
});

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[contidionlock lockWhenCondition:3];
NSLog(@"++++执行线程2");
[contidionlock unlockWithCondition:2];
});

//线程3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[contidionlock lockWhenCondition:1];
NSLog(@"++++执行线程3");
[contidionlock unlockWithCondition:3];
});
}

输出日志:

1
2
3
++++执行线程1
++++执行线程3
++++执行线程2

上片代码中,初始化 NSConditionLock 对象时,标识设置为0;运行后:

  • 线程1执行 tryLockWhenCondition:时,传入标识“0”,所以 线程1 加锁成功。
  • 线程1执行 unlockWithCondition:时,condition由 0 被修改为 1。
  • 因为condition修改为了 1,线程3 符合条件并成功上锁,之后线程3将condition 修改为3。
  • 最后线程2执行。

从上面的结果可以发现,NSConditionLock 还可以实现任务之间的依赖。

使用案例:PINRemoteImage

6.3.dispatch_semaphore

信号量:是一种用来控制资源是否可访问的数量标识,0表示没有可用信号量,>=1表是有可用信号量。在进入一段临界区代码之前,线程须获取一个信号量;临界区代码段执行完成后,该线程须释放信号量。在无可用信号量时,其它想进入该临界区的线程必须等待前面的线程释放信号量。

经典的停车场案例:

  • 信号量的值相当于剩余车位的数量;
  • dispatch_semaphore_wait相当于来了一辆车;
  • dispatch_semaphore_signal相当于走了一辆车;

停车位的剩余数目在初始化时就已经指明了(dispatch_semaphore_create(value:Int))),调用一次dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait剩余车位就减少一个;当剩余车位为0时,再来车(即调用dispatch_semaphore_wait)就只能等待。有耐心的车主会一直等下去,没耐心的车主在等待“一段时间”之后就会离开。

1
2
3
4
5
6
7
8
//创建信号量,参数:信号量的初值,如果小于0则会返回NULL
dispatch_semaphore_create(信号量值)

//减少信号量,时间:DISPATCH_TIME_NOW、DISPATCH_TIME_FOREVER
dispatch_semaphore_wait(信号量,等待时间)

//释放信号量
dispatch_semaphore_signal(信号量)

#示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void)Segmaphore
{
//初始信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_queue_t quene = dispatch_queue_create("com.M.D", DISPATCH_QUEUE_CONCURRENT);

//任务1
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"+++任务1");
sleep(1);
NSLog(@"+++完成任务1");
dispatch_semaphore_signal(semaphore);
});

//任务2
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"+++任务2");
sleep(1);
NSLog(@"+++完成任务2");
dispatch_semaphore_signal(semaphore);
});

//任务3
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"+++任务3");
sleep(1);
NSLog(@"+++完成任务3");
dispatch_semaphore_signal(semaphore);
});
}

输出日志:

1
2
3
4
5
6
+++任务1
+++完成任务1
+++任务2
+++完成任务2
+++任务3
+++完成任务3

使用案例:AFN、GPUImage

7.读写锁

一个读写锁其读是可并发的,写是排他的,因此实现读写锁可以使用具有类似特性的GCD栅栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@interface ASDLockTool ()
// 读写锁示范
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) dispatch_queue_t mQueue;
@end

@implementation ASDLockTool

@synthesize name = _name;

- (instancetype)init {
if (self = [super init]) {
_mQueue = dispatch_queue_create("mQueue", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}

// getter 需要同步,因为要立刻返回属性的值
- (NSString *)name{
__block NSString *bName;
dispatch_sync(_mQueue, ^{
bName = _name;
});
return bName;
}

// setter 需要排他性,写时不允许其他写操作或者读操作,所以使用栅栏
- (void)setName:(NSString *)name{
dispatch_barrier_async(_mQueue, ^{
_name = name;
});
}
@end

因为getter需要立刻返回当前属性的值,所以对于属性的读操作使用了“同步+并发队列”;而写操作是排他的,所以使用了“异步栅栏+并发队列”,这样就能保证写入时其他操作都自动等待。


相关参考:

#©百度百科-优先级反转


八大锁
https://davidlii.cn/2017/11/21/lock.html
作者
Davidli
发布于
2017年11月21日
许可协议