引子
一个经典的卖票场景(三个窗口同时卖10张票)可以描述多线程操作同一数据资源出现的问题。
@property(nonatomic, assign) NSInteger tickets;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
self.tickets = 10;
NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
threadA.name = @"A";
NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
threadB.name = @"B";
NSThread *threadC = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
threadC.name = @"C";
[threadA start];
[threadB start];
[threadC start];
}
- (void)saleTickets
{
NSThread *thread = [NSThread currentThread];
while (1) {
if (_tickets > 0) {
[NSThread sleepForTimeInterval:1];
_tickets--;
NSLog(@"%@ 卖出一张票 剩余票数= %ld, Thread:%@", thread.name, _tickets,[NSThread currentThread]);
} else {
NSLog(@"票卖完了 Thread:%@",[NSThread currentThread]);
break;
}
}
}
输出结果可以发现数据出现了错乱
C 卖出一张票 剩余票数= 8, Thread:{number = 7, name = C}
A 卖出一张票 剩余票数= 9, Thread:{number = 5, name = A}
B 卖出一张票 剩余票数= 7, Thread:{number = 6, name = B}
B 卖出一张票 剩余票数= 4, Thread:{number = 6, name = B}
C 卖出一张票 剩余票数= 4, Thread:{number = 7, name = C}
A 卖出一张票 剩余票数= 4, Thread:{number = 5, name = A}
A 卖出一张票 剩余票数= 1, Thread:{number = 5, name = A}
B 卖出一张票 剩余票数= 3, Thread:{number = 6, name = B}
C 卖出一张票 剩余票数= 1, Thread:{number = 7, name = C}
C 卖出一张票 剩余票数= -2, Thread:{number = 7, name = C}
A 卖出一张票 剩余票数= -2, Thread:{number = 5, name = A}
B 卖出一张票 剩余票数= -2, Thread:{number = 6, name = B}
票卖完了 Thread:{number = 7, name = C}
票卖完了 Thread:{number = 5, name = A}
票卖完了 Thread:{number = 6, name = B}
锁的总类及其效率
iOS开发中常用的锁有如下几种:
1 @synchronized
2 NSLock 对象锁
3 NSRecursiveLock 递归锁
4 NSConditionLock 条件锁
5 pthread_mutex 互斥锁(C语言)
6 dispatch_semaphore 信号量实现加锁(GCD)
7 OSSpinLock (暂不建议使用,原因见参考3)
synchronized(关键字加锁 互斥锁,性能较差不推荐使用)
使用方法
@synchronized(这里添加一个OC对象,一般使用self) {
//这里写要加锁的代
}
注意点
1.加锁的代码尽量少
2.添加的OC对象必须在多个线程中都是同一对象
3.优点是不需要显式的创建锁对象,便可以实现锁的机制。
4.@synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。
示例
- (void)saleTicketsBySynchronized
{
NSThread *thread = [NSThread currentThread];
while (1) {
@synchronized (self) {
if (_tickets > 0) {
[NSThread sleepForTimeInterval:1];
_tickets--;
NSLog(@"%@ 卖出一张票 剩余票数= %ld, Thread:%@", thread.name, _tickets,[NSThread currentThread]);
} else {
NSLog(@"票卖完了 Thread:%@",[NSThread currentThread]);
break;
}
}
}
}
NSLock(互斥锁)
注意点
1 锁定(lock)和解锁(unLock)必须配对使用
2 同一锁对象才会互斥(如果把lock的初始化放在saleTicketsByNSLock方法中,则不起作用)
NSLock类还提供tryLock方法,意思是尝试锁定,当锁定失败时,不会阻塞进程,而是会返回NO。你也可以使用lockBeforeDate:方法,意思是在指定时间之前尝试锁定,如果在指定时间前都不能锁定,也是会返回NO。
@property(nonatomic, strong) NSLock *lock;
- (void)viewDidLoad {
[super viewDidLoad];
self.lock = [NSLock new];
}
- (void)saleTicketsByNSLock
{
NSThread *thread = [NSThread currentThread];
while (1) {
[self.lock lock];
if (_tickets > 0) {
[NSThread sleepForTimeInterval:1];
_tickets--;
NSLog(@"%@ 卖出一张票 剩余票数= %ld, Thread:%@", thread.name, _tickets,[NSThread currentThread]);
} else {
NSLog(@"票卖完了 Thread:%@",[NSThread currentThread]);
break;
}
[self.lock unlock];
}
}
NSRecursiveLock 递归锁
递归锁也可以像NSLock一样使用,只是对象名称换了,这里就不列举卖票的示例了
使用锁最容易犯的一个错误就是在递归或循环中造成死锁
如下代码中,在递归方法中,锁会被多次的lock,所以自己也被阻塞了
@property(nonatomic, strong) NSLock *lock;
@property(nonatomic, strong) NSRecursiveLock *recursiveLock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.lock = [NSLock new];
self.recursiveLock = [NSRecursiveLock new];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self testMethod:5];
});
}
- (void)testMethod:(NSInteger)num
{
[_lock lock];
if (num > 0) {
[NSThread sleepForTimeInterval:1];
NSLog(@"number = %ld -- %@", num--, [NSThread currentThread]);
[self testMethod:num];
}
[_lock unlock];
}
number = 5 -- {number = 3, name = (null)}
*** -[NSLock lock]: deadlock ( '(null)')
*** Break on _NSLockError() to debug.
在testMethod
方法中将NSLock
对象换成NSRecursiveLock
对象便可解决问题。
NSRecursiveLock
类定义的锁可以在同一线程多次lock,而不会造成死锁。
递归锁会跟踪它被多少次lock
,每次成功的lock
都必须平衡调用unlock
操作。
只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得
。
- (void)testMethod:(NSInteger)num
{
[_recursiveLock lock];
if (num > 0) {
[NSThread sleepForTimeInterval:1];
NSLog(@"number = %ld -- %@", num--, [NSThread currentThread]);
[self testMethod:num];
}
[_recursiveLock unlock];
}
NSConditionLock(条件锁)
使用NSConditionLock的lock和unlock方法并不能很好地解决卖票的问题,还在研究中...
pthread_mutex(互斥锁)
需要#import
核心代码
__block pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
示例
- (void)viewDidLoad {
[super viewDidLoad];
__block NSInteger count = 10;
__block pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (1) {
pthread_mutex_lock(&mutex);
if (count > 0) {
NSLog(@"A count = %ld", (long)(count--));
sleep(1);
} else {
pthread_mutex_unlock(&mutex);
break;
}
pthread_mutex_unlock(&mutex);
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (1) {
pthread_mutex_lock(&mutex);
if (count > 0) {
NSLog(@"B count = %ld", (long)(count--));
sleep(1);
} else {
pthread_mutex_unlock(&mutex);
break;
}
pthread_mutex_unlock(&mutex);
}
});
}
dispatch_semaphore(信号量实现加锁)
核心代码
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(semaphore);
示例
- (void)viewDidLoad {
[super viewDidLoad];
__block NSInteger count = 10;
//创建信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (1) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (count > 0) {
NSLog(@"A count = %ld", (long)(count--));
sleep(1);
} else {
break;
}
dispatch_semaphore_signal(semaphore);
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (1) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (count > 0) {
NSLog(@"B count = %ld", (long)(count--));
sleep(1);
} else {
break;
}
dispatch_semaphore_signal(semaphore);
}
});
}
OSSpinLock(不建议使用)
#import
@property(nonatomic, assign) OSSpinLock pinLock;
- (void)viewDidLoad {
[super viewDidLoad];
_pinLock = OS_SPINLOCK_INIT;
}
- (void)saleTickets
{
NSThread *thread = [NSThread currentThread];
while (1) {
OSSpinLockLock(&_pinLock);
if (_tickets > 0) {
[NSThread sleepForTimeInterval:1];
_tickets--;
NSLog(@"%@ 卖出一张票 剩余票数= %ld, Thread:%@", thread.name, _tickets,[NSThread currentThread]);
} else {
NSLog(@"票卖完了 Thread:%@",[NSThread currentThread]);
break;
}
OSSpinLockUnlock(&_pinLock);
}
}
测试发现,票全部是被某一个线程全部卖掉的,不符合实际情况
参考链接
参考1
参考2
参考3