iOS线程安全——锁
摘自《iOS开发快速进阶与实战》
线程安全是iOS开发中避免不了的话题,随着多线程的使用,对于资源的竞争以及数据的操作都可能存在风险,所以有必要在操作时保证线程安全。线程安全是多线程技术的保障,而iOS中实现线程安全主要是依靠各种锁,锁的种类有很多,有各自的优缺点,需要开发者在使用中权衡利弊,选择最合适的锁来搭配多线程技术。
随着项目越来越庞大且越来越复杂,对项目中事务的处理、多线程的使用也变得尤为必要。多线程利用了CPU多核的性质,能并行执行任务,提高效率,但是随之而来也会出现一些由于多线程使用而造成的问题。锁主要可以分为几种:互斥锁,递归锁,信号量,条件锁等。锁的功能就是为了防止不同的线程同时访问同一段代码。下面简单举个例子。
现在有一个对象Person类,其中有一个NSUInteger(年龄大于等于0,为无符整型)类型的属性age。
// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatiomic, assign) NSUInteger age;
@end
当然在未赋值的情况下,age默认是0。我们在外部模拟一种多线程访问该实例方法的情况。
- (void)withoutLock {
__block Person *p = [Person new];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < 1000; i++) {
p.age++;
}
NSLog(@"%zd \n",p.age);
}];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < 1000; i++) {
p.age++;
}
NSLog(@"%zd \n",p.age);
}];
}
不要在意数值的大小,这里只是为了达到模拟的效果。可以看出,有两处代码,在不同的线程中调用了p.age++,按理想情况来说,结果应该是p的age是2000,但是分别打印两个线程代码执行完后的结果却并非如此(每次执行结果都基本不相同,所以只是以某次为例,并不是确定值)。
1170
1906
因为是不同的线程,所以不确定哪一个会先执行结束,所以分别打印了一次。可以看到,最大值是1906,表示最后p的age是1906,并没有到达2000。相信有一定基础的读者都会明白其原因,因为在该处方法中没有加锁,导致不同线程竞争资源,当A线程和B线程同时拿到age时,例如此时age的值是100,执行自增代码后,A线程和B线程都将101赋给了age,但总得有个先来后到,结果就是某一次被覆盖了,按理说两次p.age++后结果应该是102,但获取到的结果是101,这就出现了误差,所以多次这样的误差就导致最后的结果值是比2000要小的。
所以这个时候,在多线程访问同一资源时要通过锁来保证同一时刻仅有一个线程对该资源的访问,这样就可以避免上述出现的问题。iOS系统提供了多种锁来解决这样的问题,下面分别介绍一下各种锁。(在下面的例子中我们设置一个统一的次数变量totalCount,设置一个比较大的数值100000。)
NSLock
NSLock是一种最简单的锁,使用起来也比较简单方便,下面通过实际代码来看一下。
- (void)nslockTest {
__block Person *p = [Person new];
NSLock *lock = [[NSLock alloc]init];
NSLog(@"begin");
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totcalCount; i++) {
[lock lock];
p.age++;
[lock unlock];
}
NSLog(@"%zd \n",p.age);
}];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totcalCount; i++) {
[lock lock];
p.age++;
[lock unlock];
}
NSLog(@"%zd \n",p.age);
}];
}
NSLock使用起来也比较简单,用创建的实例对象调用lock和unlock方法来加锁解锁。通过打印可以看到,结果是正确的,最后的age是2000。
synchronized
这种锁是比较常用的,因为其使用方法是所有锁中最简单的,但性能却是最差的,所以对性能要求不太高的使用情景下synchronized不失为一种比较方便的锁。代码如下。
- (void)synchronizedTest {
__block Person *p = [Person new];
NSLog(@"begin");
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totcalCount; i++) {
@synchronized (p) {
p.age++;
}
}
NSLog(@"%zd \n",p.age);
}];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totcalCount; i++) {
@synchronized (p) {
p.age++;
}
}
NSLog(@"%zd \n",p.age);
}];
}
可以看出不需要创建锁,一种类似于Swift中调用一个含有尾随闭包的函数,就能实现功能。
synchronized内部实现是通过传入的对象,为其分配一个递归锁,存储在哈希表中。使用synchronized还需要有一些注意的地方,除了有性能方面的劣势,还有两个问题,一个是小括号里面需要传一个对象类型,基本数据类型不能作为参数,另一个是小括号内的这个对象参数不可为空,如果为nil,就不能保证其锁的功能。我们创建另外一个值为nil的对象,传进去:
- (void)synchronizedTest {
__block Person *p = [Person new];
__block Person *p1 = nil;
NSLog(@"begin");
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totcalCount; i++) {
@synchronized (p1) {
p.age++;
}
}
NSLog(@"%zd \n",p.age);
}];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totcalCount; i++) {
@synchronized (p1) {
p.age++;
}
}
NSLog(@"%zd \n",p.age);
}];
}
打印结果如下。
begin
113750
124617
从打印结果可以看到,两次打印并没有一次能够达到两次循环次数的总和。也就是说明如果传值nil的话,就失去了synchronized提供的锁功能。
pthread
pthread的全称是POSIX thread,是一套跨平台的多线程API,各个平台对其都有实现。pthread是一套非常强大的多线程锁,可以创建互斥锁(普通锁)、递归锁、信号量、条件锁、读写锁、once锁等,基本上所有涉及的锁,都可以用pthread来实现,下面分别对其进行举例。
1.互斥锁(普通锁)
- (void)pthreadNormalTest {
__block Person *p = [Person new];
NSLog(@"begin");
__block pthread_mutex_t t;
pthread_mutex_init(&t, NULL);
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totcalCount; i++) {
pthread_mutex_lock(&t);
p.age++;
pthread_mutex_unlock(&t);
}
NSLog(@"%zd \n",p.age);
}];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totcalCount; i++) {
pthread_mutex_lock(&t);
p.age++;
pthread_mutex_unlock(&t);
}
NSLog(@"%zd \n",p.age);
}];
}
可以看到普通的互斥锁的创建和使用也是比较简单的,但是需要注意在合适的地方对其调用方法进行销毁。
pthread_mutex_destroy(&t);
注意:在本节关于锁的实例代码中,都将锁的创建放在了方法中,但在实际开发中,多线程都是直接调用方法的,所以也就应使用同一个锁对象。为了保证锁的正常使用,一般将其设置为方法所属对象的一个属性,才能在调用该对象的方法时保证其线程安全,而不是像例子中那样在方法中创建,示例代码仅为演示效果,希望读者能够理解。
2.递归锁
递归锁的创建方法跟普通锁是同一个方法,不过需要传递一个attr参数。
- (void)pthreadRecursiveTest {
__block Person *p = [Person new];
NSLog(@"begin");
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
pthread_mutexattr_destroy(&attr);
__block pthread_mutex_t t = mutex;
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
pthread_mutex_lock(&t);
p.age++;
pthread_mutex_unlock(&t);
}
NSLog(@"% zd \n",p.age);
}];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
pthread_mutex_lock(&t);
p.age++;
pthread_mutex_unlock(&t);
}
NSLog(@"% zd \n",p.age);
}];
}
关于普通锁和递归锁的区别,后面再做陈述,这里先简单介绍锁的用法。
同样,关于pthread递归锁需要注意的是,首先对其属性需要在创建完递归锁之后释放:
pthread_mutexattr_destroy(&attr);
另外,同样也要注意在该锁所对应的对象释放的时候也要对该锁调用释放方法。
pthread_mutex_destroy(&t);
3.pthread信号量
pthread的信号量不同于GCD自带的信号量,如前面所说,pthread是跨平台多线程处理的API,对信号量处理也提供了相应的使用。其大概原理和使用方法与GCD提供的信号量机制类似,使用起来也比较方便。关于GCD的信号量在下面会单独讲到,这里是pthread信号量的使用代码。
- (void)pthreadCondTest {
__block Person *p = [Person new];
__block pthread_mutex_t t = PTHREAD_MUTEX_INITIALIZER;
__block pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
NSLog(@"begin");
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
pthread_mutex_lock(&t);
pthread_cond_wait(&cond,&t);
p.age++;
pthread_mutex_unlock(&t);
}
NSLog(@"% zd \n",p.age);
}];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
pthread_mutex_lock(&t);
p.age++;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&t);
}
NSLog(@"% zd \n",p.age);
}];
}
通过使用方法可以看到,pthread_cond_t是需要搭配pthread普通锁共同使用的,是通过pthread_cond_wait和pthread_cond_signal来实现信号量的生产和消费。但与GCD的信号量略有不同,首先pthread_cond_t需要搭配pthread普通锁一起使用,其次pthread_cond_t不能设置信号量的个数,纯粹是一个信号量锁。
pthread_cond_t也可以称为pthread状态锁,如果是第一个线程先获得调度,在第一个线程内调用pthread_mutex_lock(&t)之后,需要等一个信号量才能继续执行,此时内部会将其unlock,然后等第二个线程调度,当第二个线程完成后释放了一个信号并解锁后,线程1重新得到调度,此时在pthread_cond_wait内部重新上锁,然后继续执行线程1的代码。当消耗了这个信号量,下次线程1再获得调度时仍然会阻塞,然后周而复始。
如果读者执行上面这段代码,可以发现,控制台并没有打印两次p.age,综合刚刚的解释,可以明白,控制台打印的是线程2的NSLog,然后线程1消耗了信号量之后并没有其他信号量可以使用,所以一直处于阻塞状态。虽然在此例中并没有完全执行p.age到200 000,但这种状态锁是一种自定义的任务调度方式,可以将指定的事务交给指定的线程来处理。
可以看出,pthread使用信号量来实现线程安全也是比较方便的,通过一个宏来初始化pthread_mutex_t,在涉及锁功能时,pthread_cond_t需要注意与锁的使用搭配。
pthread_mutex_lock(&t);
pthread_cond_wait(&cond,&t);
//CODE
pthread_mutex_unlock(&t);
在调用pthread_cond_wait之前需要先上锁,因为在没有信号量可以消费的时候pthread_cond_wait会解锁,并在获得新的信号量时再次对其加锁。
pthread_mutex_lock(&t);
//CODE
pthread_cond_signal(&cond);
pthread_mutex_unlock(&t);
这段代码需要在执行完代码后先释放信号量,再对其解锁,这样线程1才能获取到锁,并且pthread_cond_wait才能对其重新上锁往下执行。如果是先释放锁,可能线程1获取到锁仍然不能执行,等再释放信号,线程1又得重新获取一遍,更有甚者,此时锁可能又被线程2抢去了。
4.读写锁
读写锁是一种特殊的自旋锁,将对资源的访问者分为读者和写者,顾名思义,读者对资源只进行读访问,而写者对资源只有写访问。相对于自旋锁来说,这种锁能提高并发性。在多核处理器操作系统中,允许多个读者访问同一资源,却只能有一个写者执行写操作,并且读写操作不能同时执行。
- (void)readWriteLockTest {
__block Person *p = [Person new];
__block pthread_rwlock_t rwl = PTHREAD_RWLOCK_INITIALIZER;
NSLog(@"begin");
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
pthread_rwlock_rdlock(&rwl);
p.age++;
pthread_rwlock_unlock(&rwl);
}
NSLog(@"% zd \n",p.age);
}];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
pthread_rwlock_wrlock(&rwl);
p.age++;
pthread_rwlock_unlock(&rwl);
}
NSLog(@"% zd \n",p.age);
}];
}
pthread的读写锁的初始化方法也是通过一个宏返回的,这是pthread为我们提供好的静态初始化宏。读写锁在具体使用的时候有三个方法,一个是给读操作上锁pthread_rwlock_rdlock,一个是给写操作上锁pthread_rwlock_wrlock,两个方法参数都是传一个读写锁的指针;最后一个方法是给读操作和写操作解锁pthread_rwlock_unlock,参数也是读写锁的指针。
这里很有意思,虽然通过打印,发现也实现了我们所要的功能,但是使用情景却是不对的,可以说是误打误撞的使用范例。原因是这样的,刚刚上面提到,读写锁是不能共存的,而且读操作是可以多个同时存在并执行的,但是写操作却只能存在一个,并且读与写也不能同时存在,读操作和其他的写操作会在当前写操作执行时,所以导致了上面的在“错误”的逻辑下却产生了“正确”的结果,因为读和写是互斥的,所以实现了锁的功能。然而这并不是正确的使用场景,正确的场景还是应该用在例如读写文件中,在上读锁跟解锁之间执行读操作,在上写锁跟解锁之间执行写操作。
所以读写锁保障了读写的安全性和有效性,并且更多的是读操作,由于这种逻辑处理,导致读写锁性能比普通锁要稍微低一点儿,但也算有比较方便的实用性。
信号量
在iOS开发中,信号量就是通过GCD来实现的,而GCD是对C语言的一个封装,不同的开发语言中对于信号量semaphore都有自己的实现,所以本节不仅是代表了pthread,也是信号量的使用,更是跨线程访问的一个主要的知识点。
信号量的使用其实很简单,与其他开发语言中使用的信号量类似,通过对信号的等待和释放来使用,信号量属于生产者消费者模式,这种模式可以用在多个使用场景中,下面只是比较常见的一种。
- (void)semaphoneTest {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
__block Person *p = [Person new];
NSLog(@"begin");
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i <totalCount; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
p.age++;
dispatch_semaphore_signal(semaphore);
}
NSLog(@"% zd\n",p.age);
}];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
p.age++;
dispatch_semaphore_signal(semaphore);
}
NSLog(@"% zd \n",p.age);
}];
}
每次在访问p.age之前,都会等待一个信号量,才能实现对age的访问,在创建方法中,需要传一个value的long类型的值,表示总共已经有了多少个信号可以使用。
举个简单的例子,办理银行业务时,银行可以设置多个窗口,这个窗口数就是信号量创建的这个value值。假设银行总共有5个窗口,相当于总共有5个资源,每有一个客户去窗口办理业务的时候,相当于消费了这个资源,当5个窗口都有客户办理业务的时候,也就是没有剩下的可用资源,那么这时候如果还有人要办理业务就得等某个窗口空出来,窗口空出来相当于释放了一个资源,这样才能接着被下一个客户使用。这就是生产者消费者理论,生产者消费者理论在多线程事件调度方面可以起到很强大的作用。如果分配的资源只有一个的时候,那么就是本节中的例子。还是刚刚举的例子,客户在同一个窗口无论如何存钱取钱都不会导致钱的数目出现差错。
所以说,当资源充足时,当代码执行到dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER)时,不用等待,而直接消耗一个资源,只有在当前没有可用的资源时,才会等待dispatch_semaphore_signal(semaphore)来释放一个可用的资源。这其中的逻辑并不复杂,就是事务竞争资源。
后面还有一个时间参数,这个比较简单,在消费者等待一个可用的资源时是有时间限制的,超过该时间就不去等待资源而直接执行下面的代码。这在此处或许有些不合逻辑,因为假设时间很小,在没有获取到信号量资源的时候就去执行代码,可能会造成非线性安全的事故,但这是系统安全的,也就是并不会造成应用崩溃。在本节关于锁的内容中,将时间设置为DISPATCH_TIME_FOREVER,表示将一直会等下去,这就确保了线程的安全。
NSConditionLock与NSCondition
1.NSConditionLock
状态锁是一种比较常用的锁,在多线程操作中,用户可以指定某线程去执行操作,只需要设定对应的状态即可。
- (void)conditionLockTest {
NSConditionLock *lock = [[NSConditionLock alloc] init];
__block Person *p = [Person new];
NSInteger thread1 = 1;
NSInteger thread2 = 0;
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
[lock lockWhenCondition:thread1];
p.age++;
[lock unlockWithCondition:thread2];
}
NSLog(@"% zd \n",p.age);
}];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i ++) {
[lock lockWhenCondition:thread2];
p.age++;
[lock unlockWithCondition:thread1];
}
NSLog(@"% zd \n",p.age);
}];
}
NSConditionLock主要有两个方法,一个是-lockWhenCondition:,一个是-unlockWithCondition:。用法很简单,表示只有在某种状态下才能上锁,操作完成后解锁并将状态更改,供下次符合条件的线程上锁。举个简单的例子,有一个男女公用的厕所,一次只能有一个人使用,厕所门上有一个标示牌,当牌子上是♂的时候,表示这个厕所现在只能男生使用,即使是女生排在第一位(CPU系统调度,但状态不符合),所以得一直找到男生,才能使用。当男生使用完,可以将厕所的标示牌随意更改为♂或者♀,接下来同上。这就是NSConditionLock的作用,在例子中,两个线程在操作完成后将状态值更改为其他值,所以两个线程能够轮流执行,通过打印结果也可以看出来,二者只相差1。
2.NSCondition
这里介绍一下与NSConditionLock类似的NSCondition,看起来两个差不多,虽然只相差一个Lock,但足以表示它们的主要用法不同。NSConditionLock在刚刚已经介绍过,NSCondition更类似于信号量的使用。虽然NSConditionLock与NSCondition在用法上略有不同,但为了达到与NSConditionLock相同的用法,这里展示与NSConditionLock做相同的事。
在展示NSCondition代码之前,我们先看一下Apple的官方文档中,对于NSCondition提供的一段伪代码。
lock the condition
while(!(boolean_predicate)) {
wait on condition
}
do protected work
(optionally, signal or broadcast the condition again or change a predicate value)
unlock the condition
通过伪代码可以看出,在使用NSCondition时,先将其上锁,当其不满足条件时,使其处于wait状态,紧接着写上一些需要做线程安全的代码,然后释放信号量,或者广播一个状态,足以break刚才的while循环,最后将其解锁。都是根据条件来上锁解锁,为了达到和NSConditionLock相同的效果,下面是展示代码。
- (void)conditionTest {
NSCondition *lock = [[NSCondition alloc]init];
__block Person *p = [Person new];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
[lock lock];
while (p.age % 2 == 0) {
[lock wait];
}
p.age++;
[lock signal];
[lock unlock];
}
NSLog(@"% zd \n",p.age);
}];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
[lock lock];
while (p.age % 2 == 1) {
[lock wait];
}
p.age++;
[lock signal];
[lock unlock];
}
NSLog(@"% zd \n",p.age);
}];
}
当p.age是单数时,线程1将处于wait状态,并且由线程2执行,反之当p.age是偶数时,线程2处于wait状态,由线程1执行,达到了与NSConditionLock例子中两个线程轮流执行的效果。通过打印,也是可以得出该结论的。
自旋锁
自旋锁在iOS系统中的实现是OSSpinLock。自旋锁通过一直处于while盲等状态,来实现只有一个线程访问数据。由于一直处于while循环,所以对CPU的占用也是比较高的,用CPU的消耗换来的好处就是自旋锁的性能很高。
然而现在不建议使用自旋锁,因为自旋锁在iOS中有bug,这个稍后将讲到,下面先介绍一下OSSpinLock的用法,虽然现在基本上不使用它。
#import <libkern/OSAtomic.h>
- (void)OSSpinLockTest {
__block OSSpinLock spinLock = OS_SPINLOCK_INIT;
__block Person *p = [Person new];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
OSSpinLockLock(&spinLock);
p.age++;
OSSpinLockUnLock(&spinLock);
}
NSLog(@"% zd \n",p.age);
}];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
OSSpinLockLock(&spinLock);
p.age++;
OSSpinLockUnLock(&spinLock);
}
NSLog(@"% zd \n",p.age);
}];
}
可以看到自旋锁的使用也是很简便的,首先需要#import <libkern/OSAtomic.h>,因为关于自旋锁的API是在这个文件中声明的。创建自旋锁也是通过一个静态宏,在线程内通过OSSpinLockLock和OSSpinLockUnlock来上锁、解锁。如果不是因为现在的OSSpinLock出现了使用bug,在性能以及使用方面来说,都是很好的使用锁的选择。下面来详细说下自旋锁。
为何自旋锁现在出现bug呢?在最近的iOS操作系统中,实现的自旋锁与自身维护线程的调度算法有冲突,是导致bug的原因。在iOS维护的线程中,有一套调度算法,会使高优先级的线程优先执行。所以当低优先级的线程获取到了自旋锁,高优先级的线程想要申请该锁,就会使高优先级线程处于while一直循环申请的状态,与低优先级的线程处理抢夺CPU处理时间,导致高优先级不能申请成功,造成死锁的状态,并且两者都不能释放。目前针对这种情况也是有处理方法的,但就会使自旋锁的使用稍显麻烦,这里不做阐述。
由于自旋锁出现了这个问题,导致在目前的开发中,很少有开发者会选择OSSpinLock来实现锁的功能,即使OSSpinLock的性能在各种所有锁的性能中是最好的,所以需要慎用。
递归锁
前面简单提及递归锁的概念,说到递归,在很多代码以及算法中某函数内部会调用自身,通过这种形式,将比较复杂的问题分解为稍容易一些的问题,再通过相同的方法来继续处理,同理一层一层分解,然后将每个返回值返回至上一层,层层返回达到最终结果。一些比较经典的关于递归的例子就是斐波那契函数、二叉树遍历等。
上面简单介绍了一下递归的概念,为下面介绍递归锁做个铺垫。其实递归锁跟递归并没有太大的关系,只是有相似的使用模型,在以上介绍锁的代码中,一个锁只是请求一份资源,而在一些开发实际中,往往需要在代码中嵌套锁的使用,也就是在同一个线程中,一个锁还没有解锁就再次加锁,这在代码编译器中不会报错以及警告,但是运行期会直接出现问题,并且不执行锁中代码。
// 这是错误代码!
- (void)wrongRecursiveTest {
NSLock *lock = [[NSLock alloc] init];
__block Person *p = [Person new];
NSLock(@"begin");
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
[lock lock];
[lock lock];
p.age++;
[lock unlock];
[lock unlock];
}
NSLog(@"% zd \n",p.age);
}];
[NSThread detachNewThreadWithBlock:^{
for (i = 0; i < totalCount; i++) {
[lock lock];
[lock lock];
p.age++;
[lock unlock];
[lock unlock];
}
NSLog(@"% zd \n",p.age);
}];
}
这是最基本的锁NSLock,我们依次举例,来表示在同一线程中多次上锁,类似于递归中在一组加锁解锁中再次加锁解锁。从代码中可以看到,连续调用了两次加锁、解锁,这只是为了达到演示目的,实际开发中这两次加锁中间可能会有其他代码,是手误也好,是业务需求使然也罢,在运行之后可以看到,并没有打印p.age的两次结果,取而代之的是一段错误log:
*** -[NSLock lock]: deadlock (<NSLock: 0x6180000d1c60>'(null)')
*** Break on _NSLockError() to debug.
可以通过控制台打印得到信息,我们在同一线程重复上锁时,会造成死锁,系统在debug模式下会自动break这段代码。这并不是我们想要的,因为已经影响了正常的代码执行,如果在业务中出现就会造成不可知的后果。以下是正确的递归锁使用代码。
- (void)recursiveTest {
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
__block Person *p = [Person new];
NSLog(@"begin");
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
[lock lock];
[lock lock];
p.age++;
[lock unlock];
[lock unlock];
}
NSLog(@"% zd \n",p.age);
}];
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < totalCount; i++) {
[lock lock];
[lock lock];
p.age++;
[lock unlock];
[lock unlock];
}
NSLog(@"% zd \n",p.age);
}];
}
当我们将NSLock换成了NSRecursiveLock,在这种递归锁下运行,可以看到代码是如期正常执行的。虽然代码执行成功了,读者可能仍然会有些困惑,还是不能明白为何递归锁会这么写,感觉在实际开发中并没有什么可以借鉴的使用场景。下面对递归锁再次举个例子。
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
void MyRecursiveFunction(int value) {
[theLock lock];
if(value != 0) {
--value;
MyRecursiveFunction(value);
}
[theLock unlock];
}
MyRecursiveFunction(5);
在这样的代码中或许读者能明白关于递归锁的实际使用场景,递归锁的使用在实际开发中也是常有的,所以需要谨慎。
小结
在本章介绍的这些锁中,可以应用于实际开发中的绝大部分使用场景,每种功能可以根据需求使用不同的锁来实现,而同一种锁根据其特性能发挥出不同的使用效果。这里重点提及一下pthread,pthread是一套跨平台的多线程API,其内部提供了丰富的API可以使用,而且NSLock以及NSConditionLock等都是基于pthread的实现,之所以将其并列出来讲解仅仅是为了阐述其锁的功能。关于pthread也只是介绍了其关于锁的一部分,可见其多么强大。而对于各个锁的优缺点在每小点中也有阐述,包括自旋锁的不安全性等,开发者应该对其有一定的了解。
在APP开发日益复杂庞大的今天,多线程的使用能有效提高应用的事务处理能力,开发者在享受多线程带来的便利的同时,也要注意多线程衍生出的线程安全问题。