iOS多线程:线程安全 常见锁 (六)
一、前言
前段时间看了几个开源项目,发现他们保持线程同步的方式各不相同,有@synchronized
、NSLock
、dispatch_semaphore
、NSCondition
、pthread_mutex
、OSSpinLock
。后来网上查了一下,发现他们的实现机制各不相同,性能也各不一样。下面我们先分别介绍每个加锁方式的使用,在使用一个案例来对他们进行性能对比。
二、非线程安全
举例说明:两个火车票销售窗口 共同销售车站总共的50张车票。看代码你最明白。
/**
* 非线程安全
* 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
*/
- (void)initTicketStatusNotSave {
NSLog(@"------开始放票了---%@",[NSThread currentThread]); // 打印当前线程
self.ticketSurplusCount = 50;
// queue1 代表北京火车票售卖窗口
dispatch_queue_t queue1 = dispatch_queue_create("com.gorpeln.testQueue1", DISPATCH_QUEUE_SERIAL);
// queue2 代表上海火车票售卖窗口
dispatch_queue_t queue2 = dispatch_queue_create("com.gorpeln.testQueue2", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) weakSelf = self;
dispatch_async(queue1, ^{
[weakSelf saleTicketNotSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketNotSafe];
});
}
/**
* 售卖火车票(非线程安全)
*/
- (void)saleTicketNotSafe {
while (1) {
if (self.ticketSurplusCount > 0) { //如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { //如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完");
break;
}
}
}
输出结果:
------开始放票了---<NSThread: 0x600003b98b00>{number = 1, name = main}
剩余票数:49 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
剩余票数:48 窗口:<NSThread: 0x600003bcc080>{number = 3, name = (null)}
剩余票数:46 窗口:<NSThread: 0x600003bcc080>{number = 3, name = (null)}
剩余票数:47 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
剩余票数:44 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
剩余票数:45 窗口:<NSThread: 0x600003bcc080>{number = 3, name = (null)}
剩余票数:43 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
......
剩余票数:7 窗口:<NSThread: 0x600003bcc080>{number = 3, name = (null)}
剩余票数:5 窗口:<NSThread: 0x600003bcc080>{number = 3, name = (null)}
剩余票数:6 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
剩余票数:3 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
剩余票数:3 窗口:<NSThread: 0x600003bcc080>{number = 3, name = (null)}
剩余票数:2 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
剩余票数:1 窗口:<NSThread: 0x600003bcc080>{number = 3, name = (null)}
所有火车票均已售完
剩余票数:0 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
所有火车票均已售完
可以看到在不考虑线程安全,得到票数是错乱的,这样显然不符合我们的需求,所以我们需要考虑线程安全问题。防止两条线程同时对此任务进行编辑,每次只能有一条线程执行此任务。所以就用到了线程加锁
三、介绍与使用
2.1、@synchronized互斥锁
/**
* 线程安全:使用 @synchronized 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/
- (void)initTicketStatusNotSave {
NSLog(@"------开始放票了---%@",[NSThread currentThread]); // 打印当前线程
self.ticketSurplusCount = 50;
// queue1 代表北京火车票售卖窗口
dispatch_queue_t queue1 = dispatch_queue_create("com.gorpeln.testQueue1", DISPATCH_QUEUE_SERIAL);
// queue2 代表上海火车票售卖窗口
dispatch_queue_t queue2 = dispatch_queue_create("com.gorpeln.testQueue2", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) weakSelf = self;
dispatch_async(queue1, ^{
[weakSelf saleTicketNotSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketNotSafe];
});
}
/**
* 售卖火车票(线程安全)
*/
- (void)saleTicketNotSafe {
while (1) {
@synchronized(self) {
if (self.ticketSurplusCount > 0) { //如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { //如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完");
break;
}
}
}
}
输出结果:
------开始放票了---<NSThread: 0x60000018a0c0>{number = 1, name = main}
剩余票数:49 窗口:<NSThread: 0x6000001deec0>{number = 4, name = (null)}
剩余票数:48 窗口:<NSThread: 0x6000001deec0>{number = 4, name = (null)}
剩余票数:47 窗口:<NSThread: 0x6000001deec0>{number = 4, name = (null)}
剩余票数:46 窗口:<NSThread: 0x6000001d4d40>{number = 5, name = (null)}
剩余票数:45 窗口:<NSThread: 0x6000001d4d40>{number = 5, name = (null)}
......
剩余票数:4 窗口:<NSThread: 0x6000001deec0>{number = 4, name = (null)}
剩余票数:3 窗口:<NSThread: 0x6000001deec0>{number = 4, name = (null)}
剩余票数:2 窗口:<NSThread: 0x6000001deec0>{number = 4, name = (null)}
剩余票数:1 窗口:<NSThread: 0x6000001deec0>{number = 4, name = (null)}
剩余票数:0 窗口:<NSThread: 0x6000001d4d40>{number = 5, name = (null)}
所有火车票均已售完
所有火车票均已售完
@synchronized
指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized
块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。@synchronized
还有一个好处就是不用担心忘记解锁了。
2.2、dispatch_semaphore
/**
* 线程安全:使用 semaphore 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/
- (void)initTicketStatusNotSave {
NSLog(@"------开始放票了---%@",[NSThread currentThread]); // 打印当前线程
semaphoreLock = dispatch_semaphore_create(1);
self.ticketSurplusCount = 50;
// queue1 代表北京火车票售卖窗口
dispatch_queue_t queue1 = dispatch_queue_create("com.gorpeln.testQueue1", DISPATCH_QUEUE_SERIAL);
// queue2 代表上海火车票售卖窗口
dispatch_queue_t queue2 = dispatch_queue_create("com.gorpeln.testQueue2", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) weakSelf = self;
dispatch_async(queue1, ^{
[weakSelf saleTicketNotSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketNotSafe];
});
}
/**
* 售卖火车票(线程安全)
*/
- (void)saleTicketNotSafe {
while (1) {
// 相当于加锁
dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
if (self.ticketSurplusCount > 0) { //如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { //如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完");
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
break;
}
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
}
}
输出结果:
------开始放票了---<NSThread: 0x600002c792c0>{number = 1, name = main}
剩余票数:49 窗口:<NSThread: 0x600002c28d00>{number = 4, name = (null)}
剩余票数:48 窗口:<NSThread: 0x600002c15100>{number = 6, name = (null)}
剩余票数:47 窗口:<NSThread: 0x600002c28d00>{number = 4, name = (null)}
剩余票数:46 窗口:<NSThread: 0x600002c15100>{number = 6, name = (null)}
剩余票数:45 窗口:<NSThread: 0x600002c28d00>{number = 4, name = (null)}
剩余票数:44 窗口:<NSThread: 0x600002c15100>{number = 6, name = (null)}
剩余票数:43 窗口:<NSThread: 0x600002c28d00>{number = 4, name = (null)}
......
剩余票数:4 窗口:<NSThread: 0x600002c15100>{number = 6, name = (null)}
剩余票数:3 窗口:<NSThread: 0x600002c28d00>{number = 4, name = (null)}
剩余票数:2 窗口:<NSThread: 0x600002c15100>{number = 6, name = (null)}
剩余票数:1 窗口:<NSThread: 0x600002c28d00>{number = 4, name = (null)}
剩余票数:0 窗口:<NSThread: 0x600002c15100>{number = 6, name = (null)}
所有火车票均已售完
所有火车票均已售完
dispatch_semaphore 是 GCD 用来同步的一种方式,dispatch_semaphore_create
是创建信号量,dispatch_semaphore_wait
是等待信号,dispatch_semaphore_signal
是发送信号。
详细请看 iOS多线程:GCD (三)
2.3、NSLock对象锁
/**
* 线程安全:使用 NSLock 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/
- (void)initTicketStatusNotSave {
NSLog(@"------开始放票了---%@",[NSThread currentThread]); // 打印当前线程
self.ticketSurplusCount = 50;
// queue1 代表北京火车票售卖窗口
dispatch_queue_t queue1 = dispatch_queue_create("com.gorpeln.testQueue1", DISPATCH_QUEUE_SERIAL);
// queue2 代表上海火车票售卖窗口
dispatch_queue_t queue2 = dispatch_queue_create("com.gorpeln.testQueue2", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) weakSelf = self;
dispatch_async(queue1, ^{
[weakSelf saleTicketNotSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketNotSafe];
});
}
/**
* 售卖火车票(线程安全)
*/
- (void)saleTicketNotSafe {
while (1) {
// 相当于加锁
[_lock lock];
if (self.ticketSurplusCount > 0) { //如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { //如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完");
break;
}
[_lock unlock];
}
}
输出结果:
------开始放票了---<NSThread: 0x600000608b80>{number = 1, name = main}
剩余票数:49 窗口:<NSThread: 0x600000651c80>{number = 4, name = (null)}
剩余票数:48 窗口:<NSThread: 0x60000066dd40>{number = 6, name = (null)}
剩余票数:47 窗口:<NSThread: 0x60000066dd40>{number = 6, name = (null)}
剩余票数:46 窗口:<NSThread: 0x600000651c80>{number = 4, name = (null)}
剩余票数:45 窗口:<NSThread: 0x600000651c80>{number = 4, name = (null)}
......
剩余票数:4 窗口:<NSThread: 0x600000651c80>{number = 4, name = (null)}
剩余票数:2 窗口:<NSThread: 0x60000066dd40>{number = 6, name = (null)}
剩余票数:3 窗口:<NSThread: 0x600000651c80>{number = 4, name = (null)}
剩余票数:1 窗口:<NSThread: 0x60000066dd40>{number = 6, name = (null)}
剩余票数:0 窗口:<NSThread: 0x600000651c80>{number = 4, name = (null)}
所有火车票均已售完
所有火车票均已售完
NSLock是Cocoa提供给我们最基本的锁对象,这也是我们经常所使用的,除lock
和unlock
方法外,NSLock还提供了tryLock
和lockBeforeDate:
两个方法,前一个方法会尝试加锁,如果锁不可用(已经被锁住),刚并不会阻塞线程,并返回NO。lockBeforeDate:
方法会在所指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。
2.4、NSRecursiveLock递归锁
NSRecursiveLock实际上定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。我们先来看一个示例:
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(2);
RecursiveMethod(value - 1);
}
[lock unlock];
};
RecursiveMethod(5);
});
这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod是递归调用的。所以每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。调试器中会输出如下信息:
输出结果:
value = 5
*** -[NSLock lock]: deadlock ( '(null)') *** Break on _NSLockError() to debug.
在这种情况下,我们就可以使用NSRecursiveLock。它可以允许同一线程多次加锁,而不会造成死锁。递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。
所以,对上面的代码进行一下改造,
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
这样,程序就能正常运行了,其输出如下所示:
value = 5
value = 4
value = 3
value = 2
value = 1
2.5、NSConditionLock条件锁
//初始化锁时,指定一个默认的条件
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
NSMutableArray *products = [NSMutableArray array];
NSInteger HAS_DATA = 1; //条件一: 有数据
NSInteger NO_DATA = 0; //条件二: 没有数据
//生产者,加锁与解锁的过程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
[lock lockWhenCondition:NO_DATA];//1. 当满足 【没有数据的条件时】进行加锁
[products addObject:[[NSObject alloc] init]];//2. 生产者生成数据
NSLog(@"produce a product,总量:%zi",products.count);
[lock unlockWithCondition:HAS_DATA];//3. 解锁,并设置新的条件,已经有数据了
sleep(1);
}
});
//消费者,加锁与解锁的过程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
NSLog(@"wait for product");
[lock lockWhenCondition:HAS_DATA];//1. 当满足 【有数据的条件时】进行加锁
[products removeObjectAtIndex:0];//2. 消费者消费数据
NSLog(@"custome a product");
[lock unlockWithCondition:NO_DATA];//3. 解锁,并设置新的条件,没有数据了
}
});
输出结果:
wait for product
produce a product,总量:1
custome a product
wait for product
produce a product,总量:1
custome a product
wait for product
produce a product,总量:1
custome a product
wait for product
produce a product,总量:1
custome a product
......
当我们在使用多线程的时候,有时一把只会lock和unlock的锁未必就能完全满足我们的使用。因为普通的锁只能关心锁与不锁,而不在乎用什么钥匙才能开锁,而我们在处理资源共享的时候,多数情况是只有满足一定条件的情况下才能打开这把锁:
在线程1中的加锁使用了lock,所以是不需要条件的,所以顺利的就锁住了,但在unlock的使用了一个整型的条件,它可以开启其它线程中正在等待这把钥匙的临界地,而线程2则需要一把被标识为2的钥匙,所以当线程1循环到最后一次的时候,才最终打开了线程2中的阻塞。但即便如此,NSConditionLock也跟其它的锁一样,是需要lock与unlock对应的,只是lock,lockWhenCondition:与unlock,unlockWithCondition:是可以随意组合的,当然这是与你的需求相关的。
2.6、NSCondition
NSCondition *condition = [[NSCondition alloc] init];
NSMutableArray *products = [NSMutableArray array];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
[condition lock];
if ([products count] == 0) {
NSLog(@"wait for product");
[condition wait];
}
[products removeObjectAtIndex:0];
NSLog(@"custome a product");
[condition unlock];
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
[condition lock];
[products addObject:[[NSObject alloc] init]];
NSLog(@"produce a product,总量:%zi",products.count);
[condition signal];
[condition unlock];
sleep(1);
}
});
输出结果:
wait for product
produce a product,总量:1
custome a product
wait for product
produce a product,总量:1
custome a product
wait for product
produce a product,总量:1
custome a product
wait for product
......
一种最基本的条件锁。手动控制线程wait和signal。
[condition lock];
一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到unlock ,才可访问
[condition unlock];
与lock 同时使用
[condition wait];
让当前线程处于等待状态
[condition signal];
CPU发信号告诉线程不用在等待,可以继续执行
不同点:
NSCondition条件量,需要一个外部共享变量,来探测条件是否满足
NSConditionLock条件锁, 不需要,条件锁自带一个探测条件,是否满足
2.7、pthread_mutex
C 语言下多线程加互斥锁的方式,那来段 C 风格的示例代码,需要 #import <pthread.h>
__block pthread_mutex_t theLock;
pthread_mutex_init(&theLock, NULL);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
pthread_mutex_lock(&theLock);
NSLog(@"需要线程同步的操作1 开始");
sleep(3);
NSLog(@"需要线程同步的操作1 结束");
pthread_mutex_unlock(&theLock);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
pthread_mutex_lock(&theLock);
NSLog(@"需要线程同步的操作2");
pthread_mutex_unlock(&theLock);
});
输出结果:
需要线程同步的操作1 开始
需要线程同步的操作1 结束
需要线程同步的操作2
1:pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t attr);
初始化锁变量mutex。attr为锁属性,NULL值为默认属性。
2:pthread_mutex_lock(pthread_mutex_t* mutex);
加锁
3:pthread_mutex_tylock(pthread_mutex_t* mutex);
加锁,但是与2不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待。
4:pthread_mutex_unlock(pthread_mutex_t* mutex);
释放锁
5:pthread_mutex_destroy(pthread_mutex_t* *mutex);
使用完后释放
2.8、pthread_mutex(recursive)
__block pthread_mutex_t theLock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&theLock, &attr);
pthread_mutexattr_destroy(&attr);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
pthread_mutex_lock(&theLock);
if (value > 0) {
NSLog(@"value = %d", value);
sleep(1);
RecursiveMethod(value - 1);
}
pthread_mutex_unlock(&theLock);
};
RecursiveMethod(5);
});
输出结果:
value = 5
value = 4
value = 3
value = 2
value = 1
这是pthread_mutex为了防止在递归的情况下出现死锁而出现的递归锁。作用和NSRecursiveLock递归锁类似。
如果使用pthread_mutex_init(&theLock, NULL);
初始化锁的话,上面的代码会出现死锁现象。如果使用递归锁的形式,则没有问题。
2.9、OSSpinLock
//#import <libkern/OSAtomic.h>
__block OSSpinLock theLock = OS_SPINLOCK_INIT;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSSpinLockLock(&theLock);
NSLog(@"需要线程同步的操作1 开始");
sleep(3);
NSLog(@"需要线程同步的操作1 结束");
OSSpinLockUnlock(&theLock);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSSpinLockLock(&theLock);
sleep(1);
NSLog(@"需要线程同步的操作2");
OSSpinLockUnlock(&theLock);
});
输出结果:
需要线程同步的操作1 开始
需要线程同步的操作1 结束
需要线程同步的操作2
OSSpinLock 是一种自旋锁,也只有加锁,解锁,尝试加锁三个方法。和 NSLock 不同的是 NSLock 请求加锁失败的话,会先轮询,但一秒过后便会使线程进入 waiting 状态,等待唤醒。而 OSSpinLock 会一直轮询,等待时会消耗大量 CPU 资源,不适用于较长时间的任务。