iOS开发中,多线程开发是个头疼的问题,最大的问题就是资源竞争问题。同一时间,多个线程对资源的读或者写都有可能造成不可预知的问题。解决这种问题的手段就是在操作资源的时候加上锁,那么常用的锁都有哪几种呢?本篇博客就来简单的说一说。

多线程卖票

- (void)sellTicket {
self.number = 5;
void(^sellTicket)() = ^() {
//其他代码
[self.lock lock];
if (self.number > 0) {
[NSThread sleepForTimeInterval:0.1];
self.number--;
NSLog(@"%@还有%ld张票",[NSThread currentThread],self.number);
}
[self.lock unlock];
//其他代码
};
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), sellTicket);
}
}

上面的代码是典型的多线程问题,现在有5张票,开启了10个窗口(三条线程),进行卖票(资源竞争),每个窗口只卖一张,卖票的时候判断了票的个数,但是输出结果依然有问题。输出如下:

2017-06-26 16:03:23.902 OCDemo[59833:17164897] <NSThread: 0x61000007bf80>{number = 22, name = (null)}还有1张票
2017-06-26 16:03:23.902 OCDemo[59833:17164888] <NSThread: 0x61000007ac80>{number = 16, name = (null)}还有0张票
2017-06-26 16:03:23.902 OCDemo[59833:17164893] <NSThread: 0x60000007c980>{number = 17, name = (null)}还有2张票
2017-06-26 16:03:23.902 OCDemo[59833:17164894] <NSThread: 0x600000076b00>{number = 19, name = (null)}还有2张票
2017-06-26 16:03:23.902 OCDemo[59833:17164895] <NSThread: 0x608000262b80>{number = 20, name = (null)}还有2张票
2017-06-26 16:03:23.902 OCDemo[59833:17164896] <NSThread: 0x600000077d40>{number = 21, name = (null)}还有2张票
2017-06-26 16:03:23.902 OCDemo[59833:17164427] <NSThread: 0x608000262c40>{number = 15, name = (null)}还有-1张票
2017-06-26 16:03:23.902 OCDemo[59833:17164892] <NSThread: 0x608000262a40>{number = 18, name = (null)}还有2张票
2017-06-26 16:03:23.902 OCDemo[59833:17164891] <NSThread: 0x61000007c280>{number = 14, name = (null)}还有-2张票
2017-06-26 16:03:23.902 OCDemo[59833:17164889] <NSThread: 0x60800007f600>{number = 13, name = (null)}还有-3张票

这就是典型的多线程问题。在同一时间内多个线程共同操作一个数据,就会发生这种问题。那么解决这种问题最常用的手段就是加锁,当然,还有其他的解决办法,例如我们可以将操作数据的代码放到串行队列中,但这篇文章主要来说一说各种锁的用法。

锁的概念

锁是最常用的同步工具,一段代码在同一时间只能允许被一个线程访问,比如线程A进入加锁代码以后,线程会持有这个锁,当其他线程进入这段代码以后就无法访问,只有线程A释放掉锁以后其他线程才能继续访问这段代码。但是,通常情况下,我们不需要将整段代码放入到锁中,只需要将操作数据的部分代码加锁即可。

NSLock

NSLock是一个简单的互斥锁,实现了NSLocking协议,主要方法如下:

- (void)lock; //加锁
- (void)unlock; //解锁
- (BOOL)tryLock; //此方法会尝试加锁,如果锁不可用(已经被锁住),则并不会阻塞线程,并返回NO。
- (BOOL)lockBeforeDate:(NSDate *)limit; //此方法会在所指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO

具体用法如下:

@property (nonatomic, strong) NSLock *lock;
- (void)sellTicket {
self.number = 5;
void(^sellTicket)() = ^() {
//其他代码
[self.lock lock];
if (self.number > 0) {
[NSThread sleepForTimeInterval:0.1];
self.number--;
NSLog(@"%@还有%ld张票",[NSThread currentThread],self.number);
}
[self.lock unlock];
//其他代码
};
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), sellTicket);
}
}

@synchronized

利用关键字synchronized将代码块同步,使用如下:

- (void)sellTicket {
self.number = 5;
void(^sellTicket)() = ^() {
//其他代码
@synchronized (self) {
if (self.number > 0) {
[NSThread sleepForTimeInterval:0.1];
self.number--;
NSLog(@"%@还有%ld张票",[NSThread currentThread],self.number);
}
}
//其他代码
};
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), sellTicket);
}
}

@synchronized使用self作为锁,当然你也可以使用其他的对象。

NSCondition

NSCondition是一个条件锁,同样实现了NSLocking协议,它和NSLock一样,也有lockunlock方法。它的基本用法和NSLock一样,这里说一下NSCondition的特殊用法。

NSCondition提供更高级的用法,方法如下:

- (void)wait; //阻塞当前线程 直到等待唤醒
- (BOOL)waitUntilDate:(NSDate *)limit; //阻塞当前线程到一定时间 之后自动唤醒
- (void)signal; //唤醒一条阻塞线程
- (void)broadcast; //唤醒所有阻塞线程

我们修改上面的买票例子,依然是开启10个窗口,每个窗口只卖一张票,如果票卖完了,就阻塞,一直等待,直到有票。

- (void)sellTicket {
self.number = 5;
void(^sellTicket)() = ^() {
//其他代码
[self.lock lock];
//卖票
[self loopTicket];
[self.lock unlock];
//其他代码
};
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), sellTicket);
}
}
- (void)loopTicket {
if (self.number > 0) {
self.number--;
NSLog(@"%@还有%ld张票",[NSThread currentThread],self.number);
}else {
//票没了 就阻塞 等待有票
[self.lock wait];
//我只等1秒 不管你有没有票我都要执行下面的代码
// [self.lock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:1000]];
if (self.number > 0) {
[self loopTicket];
}else {
NSLog(@"我不想等了");
}
}
}
- (void)addTicket {
//模拟来票了
self.number = 2;
//唤醒所有阻塞的线程
[self.lock broadcast];
//唤醒一个
//[self.lock signal];
}

调用addTicket方法增加票数 并唤醒所有的阻塞线程。

NSConditionLock

NSConditionLock也实现了NSLocking协议,但是它内部维护了一个条件,只有当锁可用并且条件满足时才会持有锁,所以NSConditionLock既是条件锁也是互斥锁, 来看NSConditionLock的方法:

- (instancetype)initWithCondition:(NSInteger)condition; //根据条件初始化锁对象
@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; // 在指定的时间之前 并根据条件尝试 持有锁 , 之后不会阻塞线程

我们现在来做这样一个例子,假如有10个卖票窗口,每个窗口只卖一张票。我们有十个加票员,只有票卖完了,才去加票,每次只加一张票。如下:

- (void)addTicket {
//只有当前锁可用 并且内部条件是0才会持有锁 否则阻塞 因为卖票的会在卖完票以后将内部条件设置为0
[self.lock lockWhenCondition:0];
self.number = 1;
NSLog(@"我加了一张票,还有%ld张票",self.number);
//释放锁 并且修改内部条件为1
[self.lock unlockWithCondition:1];
}
- (void)sellTicket {
self.number = 1;
void(^sellTicket)() = ^() {
//其他代码
//只有当前锁可用 并且锁内部条件是1的时候 才能持有锁 否则阻塞
[self.lock lockWhenCondition:1];
//卖票
[self loopTicket];
//释放锁 并且修改内部条件为0
[self.lock unlockWithCondition:0];
//其他代码
};
for (int i = 0; i < 10; i++) {
//10个卖票的
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), sellTicket);
//10个加票的
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self addTicket];
});
}
}
- (void)loopTicket {
if (self.number > 0) {
self.number--;
NSLog(@"我卖了一张票%@还有%ld张票",[NSThread currentThread],self.number);
}
}

输出结果如下:

2017-06-27 11:07:51.454 OCDemo[71778:17837792] 我卖了一张票<NSThread: 0x6000000798c0>{number = 3, name = (null)}还有0张票
2017-06-27 11:07:51.454 OCDemo[71778:17838431] 我加了一张票,还有1张票
2017-06-27 11:07:51.455 OCDemo[71778:17838433] 我卖了一张票<NSThread: 0x608000078bc0>{number = 4, name = (null)}还有0张票
2017-06-27 11:07:51.455 OCDemo[71778:17838436] 我加了一张票,还有1张票
2017-06-27 11:07:51.455 OCDemo[71778:17838437] 我卖了一张票<NSThread: 0x60000007e340>{number = 5, name = (null)}还有0张票
2017-06-27 11:07:51.455 OCDemo[71778:17838434] 我加了一张票,还有1张票
2017-06-27 11:07:51.456 OCDemo[71778:17838441] 我卖了一张票<NSThread: 0x60000007e400>{number = 6, name = (null)}还有0张票
2017-06-27 11:07:51.456 OCDemo[71778:17838431] 我加了一张票,还有1张票
2017-06-27 11:07:51.456 OCDemo[71778:17838442] 我卖了一张票<NSThread: 0x61800006f900>{number = 7, name = (null)}还有0张票
2017-06-27 11:07:51.457 OCDemo[71778:17838438] 我加了一张票,还有1张票
2017-06-27 11:07:51.457 OCDemo[71778:17838444] 我卖了一张票<NSThread: 0x60000007e440>{number = 8, name = (null)}还有0张票
2017-06-27 11:07:51.457 OCDemo[71778:17838436] 我加了一张票,还有1张票
2017-06-27 11:07:51.457 OCDemo[71778:17838445] 我卖了一张票<NSThread: 0x618000072280>{number = 9, name = (null)}还有0张票
2017-06-27 11:07:51.457 OCDemo[71778:17838433] 我加了一张票,还有1张票
2017-06-27 11:07:51.458 OCDemo[71778:17838435] 我卖了一张票<NSThread: 0x610000075040>{number = 10, name = (null)}还有0张票
2017-06-27 11:07:51.458 OCDemo[71778:17838434] 我加了一张票,还有1张票
2017-06-27 11:07:51.458 OCDemo[71778:17838446] 我卖了一张票<NSThread: 0x61800006f800>{number = 11, name = (null)}还有0张票
2017-06-27 11:07:51.458 OCDemo[71778:17837792] 我加了一张票,还有1张票
2017-06-27 11:07:51.459 OCDemo[71778:17838440] 我卖了一张票<NSThread: 0x608000078c80>{number = 12, name = (null)}还有0张票
2017-06-27 11:07:51.459 OCDemo[71778:17838437] 我加了一张票,还有1张票

NSRecursiveLock

NSRecursiveLock是一个递归锁,有时候加锁代码中存在递归调用,递归开始前加锁,递归调用开始后,反复执行加锁代码会造成死锁,这个时候可以使用递归锁来解决问题。使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程会记录获取锁和释放锁的次数,注意,只有两者平衡锁才会被最终释放。

如下,我们先模拟一下递归死锁,我们使用NSLock锁:

self.lock = [NSLock new];
- (void)loopLock {
[self.lock lock];
if (self.number > 0) {
self.number--;
NSLog(@"还有%ld张票",self.number);
//我还想卖
[self loopLock];
}
[self.lock unlock];
}

这里直接模拟主线程卖票,卖了一张以后 还想卖,结果造成死锁。如果我们使用NSRecursiveLockself.lock = [NSRecursiveLock new]将不会造成死锁。一定要注意,加锁和解锁是成对出现的。

dispatch_semaphore_t

条件信号量,dispatch_semaphore_t是GCD中的信号量,也可以解决资源抢占问题,支持信号通知和信号等待。每当发送一个信号通知,则信号量+1,每当发送一个等待信号时,信号量-1.如果信号量为0则信号会处于等待状态,直到信号量大于0开始执行。 开如下方法:

dispatch_semaphore_create(long value); 创建一个信号 初始化信号量 这里你可以随意设置
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); 等待信号 当信号的信号量为0的时候会阻塞线程 直到信号的信号量大于0 超时时间为timeout 当信号的信号量大于0 会将信号量减1
dispatch_semaphore_signal(dispatch_semaphore_t dsema); 发送一个信号量 此时信号的信号量会+1

示例如下:

@property (nonatomic, strong) dispatch_semaphore_t semaphore;
- (void)viewDidLoad {
[super viewDidLoad];
self.number = 10;
//创建一个信号量为1的信号
self.semaphore = dispatch_semaphore_create(1);
}
- (void)test {
for (int i = 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self sellTicket];
});
}
}
- (void)sellTicket {
//wait之后 信号量-1 为0 此时再进来的线程都需要等待 等待时间为DISPATCH_TIME_FOREVER 永远
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
if (self.number > 0) {
self.number--;
NSLog(@"%@还剩%ld张票",[NSThread currentThread],self.number);
}
//发送一个信号通知 这时信号量+1 为1;
dispatch_semaphore_signal(self.semaphore);
}

POSIX

POSIX是互斥所,和dispatch_semaphore_t很像,但是完全不同,POSIXUnix/Linux平台上提供的一套条件互斥锁的API。

新建一个简单的POSIX互斥锁pthread_mutex_t,引入头文件#import <pthread.h>声明并初始化一个pthread_mutex_t的结构。使用pthread_mutex_lockpthread_mutex_unlock函数。调用pthread_mutex_destroy来释放该锁的数据结构。

如下:

@interface ViewController ()
{
//声明 pthread_mutex_t
pthread_mutex_t mutex;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//初始化
pthread_mutex_init(&mutex, NULL);
}
- (void)test {
for (int i = 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self sellTicket];
});
}
}
- (void)sellTicket {
//加锁
pthread_mutex_lock(&mutex);
if (self.number > 0) {
self.number--;
NSLog(@"%@还剩%ld张票",[NSThread currentThread],self.number);
}
//解锁
pthread_mutex_unlock(&mutex);
}
- (void)dealloc {
//一定要释放
pthread_mutex_destroy(&mutex);
}

POSIX还可以创建条件锁pthread_cond_t,提供了和NSCondition一样的条件控制,初始化互斥锁同时使用pthread_cond_init来初始化条件数据结构:

// 初始化
int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
// 等待(会阻塞)
int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);
// 定时等待
int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);
// 唤醒
int pthread_cond_signal (pthread_cond_t *cond);
// 广播唤醒
int pthread_cond_broadcast (pthread_cond_t *cond);
// 销毁
int pthread_cond_destroy (pthread_cond_t *cond);

POSIX还提供了很多函数,有一套完整的API,包含Pthreads线程的创建控制等等,非常底层,可以手动处理线程的各个状态的转换即管理生命周期,甚至可以实现一套自己的多线程,感兴趣的可以继续深入了解。推荐一篇详细文章,但不是基于iOS的,是基于Linux的,但是介绍的非常详细Linux 线程锁详解

OSSpinLock

OSSpinLock是自旋锁,首先要声明的一点,OSSpinLock不再安全。主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。具体为什么不安全,请看这篇文章不再安全的 OSSpinLock;

玩法如下:

#import <libkern/OSAtomic.h>
@interface ViewController ()
{
OSSpinLock spinlock;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.number = 10;
spinlock = OS_SPINLOCK_INIT;
}
- (IBAction)test:(id)sender {
for (int i = 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self sellTicket];
});
}
}
- (void)sellTicket {
OSSpinLockLock(&spinlock);
if (self.number > 0) {
self.number--;
NSLog(@"%@还剩%ld张票",[NSThread currentThread],self.number);
}
OSSpinLockUnlock(&spinlock);
}
@end

最后放一张各个锁的性能对比图(摘自ibireme):


image

从图中可以看出,OSSpinLock的性能最好,但是它已不再安全。推荐大家使用dispatch_semaphore