iOS探索 多线程面试题分析

欢迎阅览iOS探究系列(按序阅览食用作用愈加)

  • iOS探究 alloc流程
  • iOS探究 内存对齐&malloc源码
  • iOS探究 isa初始化&指向剖析
  • iOS探究 类的结构剖析
  • iOS探究 cache_t剖析
  • iOS探究 办法的本质和办^ w e #法查找流程
  • iOS探究 动态办法解析和消息转发机制
  • iOS探究 浅尝辄止dyld加载w P & n I c o流程
  • iOS探究 类的加载过程
  • iOS探% 9 , b & y ,究 分类、类拓展的加载过程
  • iO J R + B D q N (S探究 isa面试题剖析
  • iOS探究 runtime面试题剖析
  • iOS探究 KVC原理及自定义
  • iOS探究 KVO原理及v o z +自定义
  • iOS探究 多线程原理
  • iOS探究 多线程之GCD运用
  • iOS探究 多线程之GCD底层剖/ _ e x f P ( a }
  • iOS探究 多线程之NSOperation
  • iOS探究 多线程面试题剖析
  • iOS探究 八大锁剖析

写在前面

前面四篇文章别离介绍了多线程原理GCD的运用GCDn : 6 b J = ~ x 8底层原理NSOperation,本文将剖析iOS面试中高频的多线程+ – q W j ] k面试题,希望各位看官都能答对(部分内容跟前几篇文章有点重复)

一、多线程的挑选方案

技术方案 简介 言语 线程生命周期 运用评率
pthread 一套通用的多k 4 o线程API
适用于Unix/Linux/Windows等体系
跨平台/可移植
$ f k p y A & ! 6用难度大
C ` } .序员办理 几乎不必
NSThread 运用! t ] C G; F A m C )加面向目标
简略易用,可C 5 W L e直接操作线程目标
OC 程序员办理 偶然运用
GCD 旨在替代NSThread等线程技. W s r !
充分利用设备的多核
C 主动办理 常常运用
NSOperation 根据GCD(底层是GCD)
比GCD多了一些更简略实用的功能
运用愈加面向目标
OC 主动办理 常常运用

留意:假如运用NSThready ? 3 v . 7 jperformSelector:withObject:afterDelay:时需求增加到当时线程的runloop中,因为在内部会创立一个NC E 6 +STimer

二、GC | t | , : . .D和NSOperation的比较

  • GCDNS6 n w 2Operat: a 0 % i _ion的联系如下:

    • GCDX W e ( $面向底层的C言语的API
    • NSOperation是用GCD封装构建的,是GCD的高级笼统
  • GCDNSOperation的对比方下:

    1. GCD履行功率更高,而且因为行列中履行的是由block构成的使命,这是一个轻量级的数据w ~ & | 8 ? I W结构——写起来愈加便9 Z 5 S h @
    2. GCD只支撑FIFO的行列,而NSOpration能够设置最大并发数、设置优先级、增加依靠联系等调整履行顺序
    3. NSOpration乃至能够跨行列Q + z – k 4 $ m F设置依靠联系,但是GCD只能经过设置串行行列,或者在行列内增加barrier使命才能操控履行顺序,较为杂乱
    4. NSOperation支撑KVO(面向目标)能够检测operation是否正在履行、是否结束、是否撤销
  • 实践项目中,许多时分只会用到异步操作,不会有特别杂乱的线程联系办理,所以苹v Q *果推崇的是优化完善、P / ) c运转快速的GCD
  • 假如考虑异步操作之间的事务性、顺序性、依靠联系,比方多线程并发下载,GCD需求写更多的代码来完结,而NSOperation已经内建了这些支撑
  • 不管是GCD仍是NSOperation,咱们接触的都是使命和行列,都没有直接接触到线程,事实上线程办理也的确不需u | k D L w求咱们操心,体系对于线程的创立、调度办理和开释都做得很好;而NSThread需求咱们自己去办理线程的生命周期,还要考虑线程同步、加锁问题,造成一些性能上的开销

三、多l o 2 , I O线程的@ V 1 V g { g s –运用场景

  • 异步履行
    • 将耗时操作放在子线程中,使其不堵` g ; k U `塞主线程
  • 刷新UI
    • 异步网络恳求,恳求结束dispatch_get_main_queue()回到主8 ^ O R 线程刷新UIK o g P 5
    • 同一F w t h页面多个网络恳求运用dispatch_groupj Q w 0 s l + M统一调度刷新UI
  • dispatch_once

    • 单例中运用,一个类仅有一个实例且提供一个大局u + : . k l – C拜访点
    • method-Swizzling运用保证办法只交流一次
  • dispatch_after将使命延迟加入行列
  • 栅门函数C E e A可用作同步锁
  • dispatch_semaphorec T : Y o T &_t

    • 用作锁保证线程安全
    • 操控GCD的最大并发数
  • dispatch_source定时器替代误差较大的NSTimer
  • AFNetworkingSDWebImage等闻名三方库中的NSOperation运用

四、线程池的原理

iOS探索 多线程面试题分析
  • 线程池巨细小于中心线程池巨细
    • 创立线程履行使命
  • 线程池巨细大于等于中心线程池巨细
    1. 先判别线程池工作行列是否已j e r 0 Z @ [
    2. & U n m @ P F没满就将使命push进行列
    3. 若已满时,且maximumPo& S ] o # U U ?olSize>corePoolSize,将创立新的线程来履行使命
    4. 反之则交给饱满策略去处理
参数名 代表含义
corePoolSize 线程池的基本巨细(中心线程池巨2 t 7 d x细)
maximumPool 线程池的最大巨细
keepAliveTime 线程池中超越corePoolSize树木的闲暇线程的最大存活时刻
unit keepAliveTime参数的时刻单位
workQueue 使命堵塞行列
threadFactory 新建线程的工厂
han4 u Idler 当提~ t {交的使命数超越maxmumPoolSize与workQueue之和时,
使命会交给RejectedExecutionHandler来处理

饱满策略有如下四个:

  • AbortPolicy直接抛出RejectedExecutionExeception异常来阻挠体系正常运转
  • CallerRunsPolicy将使命回退到调用者
  • DisOldestPol Y 7licy丢掉等候最久的使命
  • DisCardPolicy直接丢掉使命

五、栅门函数异同以及留意点

栅门函) * I k数两个API的异同

  • dispate 1 ^ ` $ k P : Sch_bar? 9 | [ w arier_async:能够操控l L X # d G行列中使p * 9命的履行顺序
  • dispatch_barrier_sync:不只堵塞了行列的履行,也堵塞了线程的履行

栅门函数留意点

  1. 尽量运用自定义的并; ] 5发行列:
    • 运用大局行列起不7 J ) 5 L D D到栅门函数的作用
    • 运用大局行列时因为对大局行列造成堵塞,或许致使体系其他调用大局行列的当地也堵塞从而导致溃散(并不是只有你在运用这个行列)
  2. 栅门函数只能操控同一并发行列:打个比方,平时在运用AFNetworking做网络恳求w c ;时为什么不能用栅门函数起到同步锁堵塞的作用,因为AFNetworking内部有自己的行列

六、栅门函数的读写锁

多读单写功能指的是:能够多个读者一起读取数据,而在读的时分,不能写入数据;在写的过程中不能有其他写者去写。即读者之间是并发的,写者与其他写者、读者之间是互斥的

- (id)V J TreadDataForKey:(NSString*)key {
__block id result;
dispatch_async(_concurrentQueue, ^{$ o ~ H D ~ 0 ] e
result = [self valueForKey:key];
});
return result;
}
- (void)writeData:(id)data forKey:(NSString*)k( r ] * B f 3 Uey {
dispatch_barrier_async(_concurrentQueue, ^{
[self setValue:data forKey:key];
});
}
  • 读:所有读者不知道读的先后顺序——并发行列+异步履行满意
  • 写:写的那个时刻段,不能有任何读者+其他写者
    • d| O : uispatch_barrier_as6 L e ( *ync满意:等行列中前面的读写使命都履行完了再来履行当时使命

七、GCD的并发量

不同于NSOperation中能够经过maxConcurrentOperationCount去操控并发数,GCD需求经过信号量才能达到作用

d{ $  { Rispatch_semaphore_t sem = dispatch_semaphore_creatG # ; $  _e(1);
dispatch_queue$ K X j h ) Z k 0_t queue = dispatch_queue_create("Felix", DISPATCHs ) Y_QUEUE_CONCURRENT);
for (int i = 0; i &k d w } x s llt; 10; i++) {
dispatl } Wch_async(queue, ^{
NSLog(@"当时%d----线程%@", i, [NSTz f v D L P % : 5hread currentThread]);
// 打印使命结束后信号量解锁
dispatch_semaphore_signal(sem)P I A o [ H;
});
// 因为异步履行,打印使命会较慢,所以这里信号量加锁
dispatch_semaphore_wait(sem, Dq w s ^ oISPATCH_TIME_FOREVER);
}
--------------------输出成果:-------------------
当时1----线程<NSThread: 0x600001448d40>{numb r ~ :ber = 3, name = (null)}
当时0----线程<NSThread: 0x60000140c240>{number = 6, name = (null)}
当时2----线程<NSThread: 0x600001448d40>{number = 3, name = (null)}
当时3----线程<NSThreaE 8 / M B 5 3d: 0x60000140c240>{number = 6, name = (null)}
当时4----线程<NSThreadg = , d 6 7 g h J: 0x60000140c240>{number = 6, name = (null)}
当时5----线程<NSThread: 0x600001448d40>{number = 3, name = (null)}
当时6----线程<NSThread: 0x600001448d40>{number = 3, name = (null)}
当时7----线程<NSThread: 0x60000140c240>{number = 6, name = (null)}
当时8--l J q z--线程<NSThread: 0x600001448d40>{numbe! ] L Kr = 3, name = (null)}
当时9----线程<NSThread: 0x60000140c2F r p 140>{number = 6, name = (null)}
--------------------输出成果:-------------------

在面试中更多会考验开发人员对于指定场景的多线程常识,接下来就来看看一些归纳运用

八、归纳运用一

1.下列代l P [ { 2 v 2 ^ 8码会报错吗?

int a = 0;
while (h I X E P z Ua < 5) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
a++;
});
}
  • 编译会报错Variable is no/ ? @ P A { S 1t assi$ T Z l {gnable (missing __block type specifier)
    • 这块归于block的常识
  • 捕获外界变量并进行修正需求加__block int a = 0;
    • 这块内容在接下来的block会讲到

2.下列代码的输出

__block int a = 0;
while (a < 5) {
dispatch_as, P $ [ c Tync(K z : $dispatch_geq  u V e B Dt_global_queue(0,L ! K k # k 1 7 $ 0), ^{
a++;
});
}
N{ A G e W X 1 ( 7SLo? a ! | 0g(@"%d", a)^ 9 _ . [ J U o;
  • 会输出0吗?
    • 不会,尽管是并发异步履行,但是有while在,不满意条件就不会跳出循环
  • 会输出1~4吗?
    • 不会(原因请往下看)
  • 会输出5吗?v f 1 ` +
    • 有或许(原因请往下看)
  • 会输出6~∞吗?
    • 极有或i J f

剖析:

  • 刚进入while循环时,a=0,然后进行a++
  • 因为是异步并发会拓荒子线程并有或许超车完结
    • 线程2a1 N v % M Y W=0履行a++时,线程3有或许已经完结了a++使a=1
    • 因为是操作同一片内存空间,线程3修正[ ? [ 6 6 C U ha导致线程2a的值也发生了改变
    • 慢一: P o m s } : O拍的线程2对已经是a=1进行a++操作
  • 同理还有线程4线程5线程n的存在
    • 能够这么理解,线程2、3、4、5、6一起在a=0时操作a
    • 线程2、3、4、5按顺序完结了操作,此刻a=4
    • 然后线程6开端操作了,但是它还没履行完就跳到了下一次循环了拓荒了线程7开端aV u |++
    • 线程6履行结束修正a=5之后来到while条件判别就会跳出循环
    • 但是I/O输出比较耗时,此5 3 w ] v v C !刻线程7又刚好完结了再打印,就会输出大于5
  • 也有那么种抱负状况B z X 5 H ` U异步并发都比较听话,刚好在a=5时没有子线程
    • 此刻就会输出5

假如还没有理解能够在while循环中) 7 D增加打印代码

__block int a = 0;
while (a < 5), } h i I s . f {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"%d————%@", a, [NSThread currentThread]);
a++;
});
}
NSLog(@"此刻的%T . [ - 5 7d", a);
iOS探索 多线程面试题分析

打印信息证明while* H n 7 %外面的打印已经履行,但是子线程仍是有 3 B a 6 ( $或许在对a进行操作的

3.怎样处理线程不安全?

或许有的小伙伴说这种需求不存在,但是咱们只管处理便是了

此刻咱们应该能想到一下几种处理方案:

  • 同步函数替换异步函数
  • 运用栅门函数
  • 运用信号量
  1. 同步函数替换异步函数
  • 成果:能满意需求
  • Q ! * s h ] c z S用:不是很好——能运用9 } 异步函数去使唤子线程为什么不必呢(尽管会消耗内存,但是功率高)
  1. 运用栅门函数
  • 成果:能满意需求
  • 作用:一般
    • 首先栅门函数大局行列搭配D v & E k { 4 9 E9 6 $ # 4 N用会无效,需求更换行列类型;
    • 其次dispatch_barrier_syncp ? O K { S Q Y Z会堵塞线程,影响性能
    • dispatch_barrier_async不能满意需求,它只能操控前面的使命履行结束再履行栅门使命(操控使命履行)但是异步栅门履行也是在子线程中,当a=4时会先持续下一次循环增加使命到行列中,再来异步履行栅门使命(不能操控使命的增加)
__block int a = 0;
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
while (a &T % O H _ : Nlt; 5) {
dispatch_async(queue, ^{
a++;
});g z # 5 5
dispatch_barrier_async(queue, ^{});
}
NSLog(@"此刻的%d", a);
sleep(1);
NSLog(@"此刻的%d", a);
--------------------输出成果:-------------------
此l } M $  Q (刻的2 * c .5
此刻的17
--------------------输出成果:-------% ^ . / ! 0------------
  1. 运用信号量
  • 成果:能满意需求
  • 作用:很好、简洁功率高
_x _ M_blX o ^ e ~ ` C 5 Eock int a = 0;
dispatch_semaphore_t sem = disp_ L Y Qatch_semaphore_creB K V D P date(0);
while (a < 5) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
a++;
dispatch_si ] +emaphore_signal(sem);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}
NSLog(@"此刻的%d", a);
sleep(1);
NSLog(@"此刻的%d", a);
--------------------输出成果:-------------------
此刻的5
此刻的5
--------------------输出成果:----------X v k E g E * ? u---------

. m % ^ Y N、归纳运用二

1.输出内容

dispatch_queue_t qV { ]ueue = dispa} P ) + o a Ptch_queue_creaO | -  W O ` S ~te("Felix", DISPATCH_QUEX u ` ( 8 jUE_CONCURRENT);
NSMutableArray *marr = @[].mutableCopy;
for* % r B E (int i =k V r ( 0; i < 1000; i++) {
dispatch_asyW M 9 N D +nc(queue, ^{
[marr addObject:@(i)];
});? ? } ; A F J G )
}
NSLog(@"%lu", mar? g ? mr.count);
  • 你:输出一个小于1000的数,因为for循环中是异步操作
  • 面试官:回去等消息吧
  • 然后你回去之后试了下大吃一惊——程序崩了

这是为什么呢?

其实跟归纳运用一是相同的道理——for循环异步时无数条线程拜访数组,造成了线程不安全

2.怎样处理线程不安全?

  • 运用串行行列
diY E  U 2 Aspatch_queue_t qu4 z * 4 Leue = dispatch_queue_create("Felix", DISPATCH_QUEUE_SERIAL);
NSL N T v . D M YMutableArray *marr = @[].muP 9 V 2 !tableCop@ I z % yy;
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
[marr addObjecA ~ ?t:@(i)];
});
}
NSLog(@"%l{ b , / (u", marr.count);
--------------------输出成果:-------------------
998
-------------[ + Z Z [ 1 m 6 y----6 , f n 2 Q v - )---输出成果:--D w ) 6 . q Q-----------------
  • 运用互斥锁
dispatch_queue_t queue = dispatch_queue_create(1 q M t"Felix", DISPATCH_QUEUE_CONCU5 l & B :RRENT);
NSMutableArray *marr = @[].mutableCi 1 R i opy;
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
@synchronized (self) {
[marr addObject:@(i)];
}
});
}
NSLog(@"%lu"E J 7 , 3 . l 9 v, marr.co: 9 [ K + Xunt). Q g [ O X H;
--------v O y H O #------------输出成果:-------------------
997
--------------n 2 } j b `------输出成果:-F )  8 6------------------
  • 运用栅门函数
di~ 7 : U a A w ? 8spatch_queue_t queue = dispatch_queue_create("Felix", DISP/ n 0ATCH_QUEUE_CONCURRENT);
NSMutable0 D Z XArrayZ g O $ B V Q O u *marrP s a y 9 ! 8 p g = @[].mutableCopy;
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
[] b # V ^ 6 y Smarr addObject:@(i)];
});
dispatch_barrier_async(queue, ^{});
}
NSLog(@"%lu", marr.count);

3.剖析思路

单路千万条,跳跳通罗马——当然除了这三种还有其他办法

  • 运用串行行列
    • 尽管功率低,但总之能处理线程安全问题
    • 尽管串行异步是使命一个接一个履行,但那是$ ; 0 ] F Z行列中的使命才满意履行规则
    • 要想得到打印成果1000,能p h J : f N p ~够在行列中履行
    • 总的来说,能满意需求` . o但不是很有效
  • 运用互斥锁
    • @synchronized: j i ) S ` ,个好东西,简略易用还有效,但也没有满意咱们的需求
    • 在for循环外运用行列内同步/异步都不能得到100
    • 要么先sleea * Zp一秒——这样不可控的代码是不可取的3 5 d j h D
    • 且在iOS^ Q T i W p S J的锁家族中@synchronized? I 4 V功率很低
  • 运用栅门函数
    • 栅门函数能够有效的操控使命的履行
    • 且与归纳运用一不同,本题中是for循环
    • 至于怎样得到打印成果1u G M T 1 c - S k000,只需求在同一行列中打印即可(栅门函| M X Z + f数的V m ~ e 2c 1 i ` U U Z :意点)
dispatch_que5 2 3 g 2 : O eue_t queue = dispatch_queue_create("Felix", DISPATS G } L = ]CH_QUEUE_CONCURRENT);
NSMuta2 Y L E K W mblI i # N L ^ D veArray *marr = @[].mutableCopy;
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
[marr addObject:@(i)];
});
dispatch_barriY d { r X | Ter_async(queue, ^{});
}
di? P E 2 Z : uspatch_async(queue, ^{
N G T YSLog(@"%lu", mS 2 3 %arr.count);
});

写在后边

多线程在日常开发中占有不少份量,一起面试中也是必问模块。但只有基X e 3础常识是一成不变的,归纳运用题稍有改动就是别的一种类型的常识考量了,而且也有多种处理方案