在开发过程中,使用多线程来可以提高程序运行效率。本文不说多线程,重点说说锁的使用。
什么时候需要用到锁呢?
比如相亲,多少单身狗的痛。你经过七大姑八大姨的介绍,争取到了一个相亲的机会,于是你就屁颠屁颠的去见人家姑娘了。结果殊不知,等你到了人家姑娘的家中后,发现她正在和另一个童鞋相谈甚欢,这个时候你能进去见人家姑娘吗?显然不能。可能她的妈妈就在门口看着呢。此处的妈妈的职责就是保证正在进行相亲不会因为其他相亲者到来而被中断,此时,你应该怎么办,等呗!你必须等那位童鞋相亲走了,你才可以进去和人家闺女相亲。
iOS中的锁就相当于妈妈,当一个线程访问数据的时候,其他的线程就必须等到该线程访问完毕以后才可访问。
如果在同一时刻多个线程对同一数据进行访问,会出现不可预期的结果,这就造成了线程不安全。比如,一个线程正在写入数据,如果这个时候有另一个线程来读取数据,那么获取到的数据将是不可预期的,也不是你真正想要的数据。
为了线程在访问时的安全性,我们有必要使用锁来保证。iOS开发中,常用的锁有一下几种:
- NSLock
- NSRecursiveLock
- NSCondition
- NSConditionLock
- pthread_mutex
- pthread_rwlock
- POSIX Conditions
- OSSpinLock
- os_unfair_lock
- @synchronized
NSLock
NSLock实现了最基本的互斥锁,遵循了NSLocking协议,通过lock和unlock加锁和解锁。
@interface ViewController ()
@property (strong, nonatomic) NSLock *lock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.lock = [[NSLock alloc] init];
NSThread *yourThread = [[NSThread alloc] initWithTarget:self selector:@selector(blindDate) object:nil];
yourThread.name = @"你";
NSThread *otherThead = [[NSThread alloc] initWithTarget:self selector:@selector(blindDate) object:nil];
otherThead.name = @"别人";
[yourThread start];
[otherThead start];
}
- (void)blindDate {
[self.lock lock];
NSLog(@"%@正在相亲", [NSThread currentThread].name);
sleep(2);
NSLog(@"%@相亲结束", [NSThread currentThread].name);
[self.lock unlock];
}
如果我们把锁撤掉:
是不是乱套了,妹纸此时此刻应该是一脸懵逼的状态。
NSRecursiveLock
递归锁,该锁可以被一个线程多次获取,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获取锁,就必须有与其对应的释放锁,保证不会出现死锁。并且只有所有的锁被释放之后,其他线程才可以继续获得锁。
self.lock = [[NSRecursiveLock alloc] init];
NSThread *interviewThread1 = [[NSThread alloc] initWithTarget:self selector:@selector(interview) object:nil];
interviewThread1.name = @"面试官 Jack";
[interviewThread1 start];
NSThread *interviewThread2 = [[NSThread alloc] initWithTarget:self selector:@selector(interview) object:nil];
interviewThread2.name = @"面试官 Rose";
[interviewThread2 start];
- (void)interview {
for (int i = 1; i < 4; i++) {
[self.lock lock];
NSLog(@"候选人%d正在面试,%@忙碌", i, [NSThread currentThread].name);
sleep(2);
NSLog(@"候选人%d面试结束,%@空闲", i, [NSThread currentThread].name);
[self.lock unlock];
}
}
从输出结果看到,加锁之前,两个面试官同时面试一个候选人。而在加锁之后,面试场景变为初试和复试,先由一个面试官Jack负责初试,由面试管Rose负责复试。这个面试的场景可能放在这里欠妥,我们主要了解递归锁的使用方法。
NSCondition
NSCondition是一种特殊的锁,它可以实现不同线程间的调度。线程1因不满足某一个条件而遭到阻塞,直到线程2满足该条件从而发出放行信号给线程1,此后,线程1才可以被正确执行。
下面我们模拟图片下载到处理的过程,开辟一个线程远程网络下载图片,开辟另一个线程处理下载好的图片,由于处理图片的线程因没有图片,而被限行,只有在图片下载完成后,才去处理图片。这样就可以在下载线程完成图片下载后发出一个信号,让另一个线程在拿到图片后在其线程上处理图片。
self.condition = [[NSCondition alloc] init];
NSThread *downloadImageThread = [[NSThread alloc] initWithTarget:self selector:@selector(downloadImage) object:nil];
downloadImageThread.name = @"下载图片";
NSThread *dealWithImageThread = [[NSThread alloc] initWithTarget:self selector:@selector(dealWithImage) object:nil];
dealWithImageThread.name = @"处理图片";
[downloadImageThread start];
[dealWithImageThread start];
static BOOL finished = NO;
- (void)downloadImage {
[self.condition lock];
NSLog(@"正在下载图片...");
sleep(2);
NSLog(@"图片下载完成");
finished = YES;
if (finished) {
[self.condition signal];
[self.condition unlock];
}
}
- (void)dealWithImage {
[self.condition lock];
while (!finished) {
[self.condition wait];
}
NSLog(@"正在处理图片...");
sleep(2);
NSLog(@"图片处理完成");
[self.condition unlock];
}
NSConditionLock
NSConditionLock互斥锁跟NSCondition很像,可以在某种条件下进行加锁和解锁,但实现方式不同。当两个线程需要特定顺序执行的时候,就可以使用NSConditionLock。如生产者消费者模型,当生产者执行的时候,消费者可以通过特定的条件获得锁;当生产者完成时解锁,然后把说的条件设置成唤醒消费者线程的条件。
加锁和解锁调用可以随意组合,lock和unlockWithCondition:配合使用,lockWhenCondition:和unlock配合使用。
@interface ViewController ()
@property (strong, nonatomic) NSConditionLock *conditionLock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.conditionLock = [[NSConditionLock alloc] init];
NSThread *producerThread = [[NSThread alloc] initWithTarget:self selector:@selector(producer) object:nil];
producerThread.name = @"生产者";
NSThread *consumerThread = [[NSThread alloc] initWithTarget:self selector:@selector(consumer) object:nil];
consumerThread.name = @"消费者";
[producerThread start];
[consumerThread start];
}
- (void)producer {
[self.conditionLock lock];
NSLog(@"生产商品中...");
sleep(2);
NSLog(@"商品生产完成");
[self.conditionLock unlockWithCondition:2];
}
- (void)consumer {
[self.conditionLock lockWhenCondition:2];
sleep(2);
NSLog(@"消费者使用商品");
[self.conditionLock unlock];
}
当生产者释放锁时,将条件设置为2,消费者就可以通过此条件获得锁,进而程序执行。如果生产者和消费者给出的条件不一致,会导致程序不能正常执行。
pthread_mutex
POSIX互斥锁,在使用时,只需初始化一个pthread_mutex_t,利用pthread_mutex_lock和pthread_mutex_unlock来加锁解锁,使用完成后,需要使用pthread_mutex_destroy来销毁锁。
#import "ViewController.h"
#import <pthread.h>
@interface ViewController ()
@end
@implementation ViewController {
pthread_mutex_t mutex_lock;
}
- (void)viewDidLoad {
[super viewDidLoad];
pthread_mutex_init(&mutex_lock,NULL);
pthread_mutex_lock(&mutex_lock);
// .....
pthread_mutex_unlock(&mutex_lock);
pthread_mutex_destroy(&mutex_lock);
}
pthread_rwlock
读写锁,当我们对文件数据进行读写操作时,写操作是排他的。一旦有多个线程对同一文件数据进行写操作,那后果不可预期。多个线程对同一文件读取时是可行的。
当读写锁被一个线程以读的模式占用时,写操作的其他线程就会被阻塞,但读操作的其他线程可以继续工作。
当读写锁被一个线程以写的模式占用时,写操作的其他线程会被阻塞,读操作的其他线程也被堵塞。
#import "ViewController.h"
#import <pthread.h>
@interface ViewController ()
@end
@implementation ViewController {
pthread_rwlock_t rwlock;
}
- (void)viewDidLoad {
[super viewDidLoad];
pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER;
rwlock = lock;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self readDataWithFlag:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self readDataWithFlag:2];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self writeDataWithFlag:3];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self writeDataWithFlag:4];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self readDataWithFlag:5];
});
}
- (void)readDataWithFlag:(NSInteger)flag {
pthread_rwlock_rdlock(&rwlock);
NSLog(@"开始读取文件数据 -- %ld", flag);
sleep(2);
NSLog(@"读取完成 -- %ld", flag);
pthread_rwlock_unlock(&rwlock);
}
- (void)writeDataWithFlag:(NSInteger)flag {
pthread_rwlock_wrlock(&rwlock);
NSLog(@"开始写入文件数据 -- %ld", flag);
sleep(2);
NSLog(@"写入完成 -- %ld", flag);
pthread_rwlock_unlock(&rwlock);
}
POSIX Conditions
POSIX条件锁 = 互斥锁 + 条件。初始化条件和互斥锁,当ready_to_go为flase的时候,进入循环,然后线程将会挂起,直到另一个线程将ready_to_go设置为ture的时候,并且发送信号的时候,该线程才会被唤醒。
#import "ViewController.h"
#import <pthread.h>
@interface ViewController ()
@end
@implementation ViewController {
pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean ready_to_go;
}
- (void)condInit {
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&condition, NULL);
ready_to_go = true;
}
- (void)waitWhenConditionCompleted {
pthread_mutex_lock(&mutex);
while (ready_to_go == false) {
pthread_cond_wait(&condition, &mutex);
}
// insert your code ...
ready_to_go = false;
pthread_mutex_unlock(&mutex);
}
- (void)signalThreadUsingCondition {
pthread_mutex_lock(&mutex);
ready_to_go = true;
pthread_cond_signal(&condition);
pthread_mutex_unlock(&mutex);
}
OSSpinLock
自旋锁,与互斥锁类似。但二者还是有区别的:
- 互斥锁,当一个线程获得这个锁后,其他想要获得此锁的线程将会被阻塞,直到该锁被释放。
- 自旋锁,当一个线程获得这个锁后,其他线程将会一直循环检测,可该锁时候被解锁。
因此,自旋锁试用于持有者在较短的情况下持有该锁。
#import "ViewController.h"
#import <libkern/OSAtomic.h>
@interface ViewController ()
@end
@implementation ViewController {
OSSpinLock spinLock;
}
- (void)viewDidLoad {
[super viewDidLoad];
spinLock = OS_SPINLOCK_INIT;
OSSpinLockLock(&spinLock);
OSSpinLockUnlock(&spinLock);
}
当然,YYKit作者也有提到过,这个自旋锁存在优先级反转的问题。因此这种锁也不再安全。详情请猛戳此处
在iOS10.0后,Apple也将其弃用了。
os_unfair_lock
OSSpinLock因存在优先级反转的问题而使得其不再安全,为此Apple推出了os_unfair_lock_t,用于解决反转的问题。
#import "ViewController.h"
#import <os/lock.h>
@interface ViewController ()
@end
@implementation ViewController {
}
- (void)viewDidLoad {
[super viewDidLoad];
os_unfair_lock_t unfair_lock;
unfair_lock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(unfair_lock);
// insert your code ...
os_unfair_lock_unlock(unfair_lock);
}
dispatch_semaphore
利用信号量的机制实现锁的功能,同样也是等待信号和发送信号的过程。当多个线程访问时,只要有一个获得信号,那么其他线程就必须等待该信号释放。
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController {
dispatch_semaphore_t semaphore_t;
}
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_semaphore_wait(semaphore_t, DISPATCH_TIME_FOREVER);
// insert your code ...
dispatch_semaphore_signal(semaphore_t);
}
@synchronized
便捷的互斥锁。如果你在多个线程中传过去的是同一个标识符,那么先获得锁的会锁定其中的代码块,其他线程将会被阻塞,反之亦然。但使用这种锁,程序效率比较低,所以我们更多的会使用其他方式的锁来代替。
@synchronized (self) {
// insert your code ...
}
总结
- 如果进行文件读写操作,使用
pthread_rwlock
比较好。因为文件读写通常会消耗大量资源,如果使用互斥锁,在多个线程同时读取文件的时候,会阻塞其他读文件的线程,而pthread_rwlock
则不会这样,所以使用其进行文件的读写,会更合适一点。 - 如果用到互斥锁,当性能要求比较高,可使用
pthread_mutex
或者dispath_semaphore
。
参考