iOS面试题(二十五)多线程 --NSOperation和NSOperationQueue&NSThread&锁机制
7.多线程
- GCD(使用最多)
- NSOperation/NSOperationQueue(AFNetworking源码中所有网络请求任务都封装到NSOperation,提交到operationQueue中,SDWebImage也会涉及)
- NSThread(实现常驻线程)
- 线程同步、资源共享(在我们实际运用多线程运用技术过程中,所产生或者引发的线程同步、资源共享问题)
- 互斥锁、自旋锁、递归锁等相关锁的一些技术内容
NSOperation
需要结合NSOperationQueue共同实现多线程
NSOperation方式去实现多线程技术方案,它的优势和特点?
1.可以添加任务依赖( addDependency/removeDependency 方法为响应的任务添加/移除依赖) 是GCD和NSThread不具备的
2.可以控制NSOperation任务的执行状态(可以重写对应的方法来实现对应状态的控制)
3.可以控制最大并发量 _queue.maxConcurrentOperationCount 设置为1,就相当于串行的任务执行方式。
任务执行状态控制
isReady 当前任务是否处于就绪状态
isExecuting 当前任务是否处于正在执行中状态
isFinished 当前任务是否已执行完毕
isCancelled 当前任务是否已取消
下面看它是如何控制状态的
1.如果只重写了NSOperation的main方法,底层会为我们控制变更任务执行完成状态,以及任务退出(后续线程的退出和NSOperation的退出)
2.如果重写了NSOperation的start方法,需要我们自行控制任务状态,在合适的时机去修改对应的isFinished等
- 查看NSOperation的start方法源码,理解上面两点
start方法内,首先创造一个自动释放池,然后获取线程优先级
做一系列的状态异常判断,然后判断当前状态是否isExecuting
如果不是,那么我们手动变成isExecuting,然后判断当前任务是否有被取消
若未被取消就调用NSOperation的main方法
再之后,调用NSOperation的finish方法。finish 方法中:在内部通过KVO的方式去变更isExecuting状态为isFinished状态
之后调用自动释放池的release
所以系统是在start方法里面为我们维护了任务状态的变更,若重写start,则没人帮我们维护了,只能自己手动维护
系统是怎样移除一个isFinished = YES的NSoperation的?
通过上面源码知道,是通过KVO方式来移除NSOperationQueue中的NSOperation的
NSThread
常用它结合RunLoop一起实现常驻线程
当我们创建一个NSThread后,会手动调用start()方法来启动线程,那么它的内部实现机制是什么呢?
- start()内部会创建pthread线程,同时指定这个线程的启动函数,
在启动函数中,通知观察者当前线程已经启动,设置线程名称,然后会调用NSThread的main()函数,
之后再调用线程关闭函数来关闭线程 - 如果main里面什么都不做,那么这个线程启动后,执行完一段逻辑就退出了,
如果我们想实现常驻线程,就需要在main里面去做一个Runloop的循环 - 下面看系统关于main的实现是什么,main内部先会做一个异常判断,
之后只有这一句调用[_target performSelector:_selector withObject:_arg], 会执行我们在创建NSThread时候所指定的选择器,也就是下图中的runRequest方法,就可以让我们在外部手动维护一个事件循环,进而实现常驻线程了
锁(线程安全问题)
例如卖票系统,100张票,好几个票点同时卖,数据就不对了
造成这个的原因是多线程的资源抢夺
还有一个情况,就是你在查找数据库的过程中,又对数据库进行增删改查了,也是线程之间的资源抢夺问题
为了解决这种问题,需要加锁来解决
在iOS当中都有哪些锁机制?
自旋锁和互斥锁的区别:
当发现有线程在操作任务时
自旋锁(OSSpinLock)一直在转,忙等,一直问你做完了没有,等线程出来后它就会立刻进去执行,适合代码量较小,耗时较小的
互斥锁会打盹,等线程出来后,会先醒盹,然后再执行
读写锁:多读单写,允许多条线程读,但只允许一条线程写,写影响读,读不影响写
例如很多地方都能调用set方法例如self.name,可以在set里面加一把互斥锁,保证在同一时间只有一条线程写入,其他线程等待
读的时候不加锁,随便读
- (void)setName:(NSString*)name{
synchronized(self){
_name = name;
}
}
常见的几种锁
1.互斥锁 @synchronized(self){里面写需要上锁的代码},一般在创建单例对象的时候使用,来保证多线程情况下创建的对象是唯一的
互斥锁分递归锁和非递归锁,这是递归锁,非递归就是当你重复访问资源时会崩溃
2. atomic原子性,也是自旋锁
3. OSSpinLock自旋锁
4. NSLock
5. NSRecursiveLock递归锁
6. dispatch_semaphore_t信号量
1. @synchronized
一般在创建单例对象的时候使用,来保证多线程情况下创建的对象是唯一的
2. atomic
修饰属性的关键字,可以对被修饰对象进行原子操作(不负责使用,意思是赋值操作可以保证线程安全,但其他操作例如增删改不能保证线程安全,需要我们做额外的线程保护)
虽然是线程安全的,但因为是自旋锁,一直忙等,会消耗大量的资源
3. OSSpinLock自旋锁: 尽量不要用,会产生一个优先级的问题,相对不再安全了
专门用于轻量级的数据访问,简单的int值,对引用计数进行+1-1操作
循环等待访问,并不释放当前资源,类似于一个while循环,一直在访问能不能获得当前这个锁,如果不能获得,就继续循环,直到有一次能获得到这个锁,才会停止这个循环
如果第一次获得到了,后续线程再想获得这个锁,是获取不到的,它会释放所持有的当前资源,然后对自己进行一个阻塞行为
4. NSLock
最经常用的,一般用来解决线程同步问题,lock是加锁,unlock是解锁,必须成对出现,但是可能会造成死锁
5. NSRecursiveLock递归锁
可以解决上面问题,它的特性是可以重入
NSLock iOS中对于资源抢占的问题可以使用同步锁NSLock来解决,使用时把需要加锁的代码(以后暂时称这段代码为”加锁代码“)放到NSLock的lock和unlock之间,一个线程A进入加锁代码之后由于已经加锁,另一个线程B就无法访问,只有等待前一个线程A执行完加锁代码后解锁,B线程才能访问加锁代码。
NSRecursiveLock :递归锁,有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁,这个时候可以使用递归锁来解决。使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程中会记录获取锁和释放锁的次数,只有最后两者平衡锁才被最终释放。
6. dispatch_semaphore_t信号量
是持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆,计数为0时等待,不可通过。计数为1或大于1时,计数减1且不等待,可通过。
也是用来实现线程同步,包括对共享资源互斥访问的一个信号量机制
信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量。
注意: 信号量可以设置线程的并发量
若信号量设置并发线程为1,也不会出现资源抢夺,因为每次只出来一条线程
相关函数
dispatch_semaphore_create(1)
创建一个Semaphore并初始化信号的总量
函数内部创建了一个结构体Semaphore,里面一个是信号量的值,第二个是线程列表
dispatch_semaphore_wait(semaphone,DISPATCH_TIME_FOREVER)
可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。,参数(要等待的信号量是哪个,等待时长)
对value值进行-1,-1之后若小于0,意味着当前没有资源可以访问,或者说无法获取这个信号量,于是当前获取信号量的这个线程会主动把自己阻塞在S.List当中
dispatch_semaphore_signal(semaphore)
发送一个信号,让信号总量加1
总结:
怎样利用GCD实现多读单写呢(怎么实现一个多读单写的模型)?
通过栅栏异步调用的方式,分配写操作到并发队列当中。
iOS系统为我们提供几种多线程技术各自的特点是怎样的?
主要提供了三种多线程技术,分别为GCD、NSOperation和NSOperationQueue、NSThread。
GCD用来实现一些简单的线程同步、子线程的分派、多读单写的解决。
NSOperation和NSOperationQueue:像AFNetworking、SDWebImageView都会涉及到NSOperation,方便对任务的状态进行控制,以及添加、移除依赖。
NSThread实现常驻线程。
NSOperation对象在Finished后,是怎样从的queue当中移除掉的?
内部通过KVO的方式来通知NSOperationQueue,然后达到对NSOperation对象进行移除的问题。
你都用过哪些锁,结合实际谈谈你是怎样使用的?
- @synchronized:一般在创建单例对象的时候使用,来保证在多线程环境下被创建的对象是唯一的。
- atomic:
- 修饰属性的关键字
- 对被修饰对象进行原子操作,即赋值时可以保证安全,但不负责使用。
- OSSpinLock自旋锁:循环等待询问,不释放当前资源。用于轻量级数据访问,比如简单的int值 +1/-1操作。
- NSRecursiveLock递归锁:可以重入,可以解决NSLock重入导致的死锁。
ps:A方法调用B方法,两个方法都进行加锁操作的话,就涉及到锁重入的问题。就用递归锁来解决。 - NSLock:解决线程同步问题,来保证各个线程互斥进入自己的临界区。
- dispatch_semaphore_t信号量:create、wait、signal:调用信号量阻塞是一个主动行为。唤醒是一个被动行为。