iOS面试剖析

多线程相关知识点

2019-10-08  本文已影响0人  huoshe2019
图1 知识结构

一、知识结构分析

整体知识架构
备注:
从上到下,更加面向对象,也就更容易使用。

多线程之间的关系

问题1:AFNetWorking为什么使用NSOperation,为什么不使用GCD?

解释:
此道题目也可以理解为NSOperation比GCD的有点。
1、NSOperation是基于GCD更高一层的封装,完全面向对象;比GCD更加简单易用,代码可读性高。
2、 FIFO队列,而NSOperationQueue中的队列可以被重新设置优先级,从而实现不同操作的执行顺序调整。CGD不具备
3、添加操作之间的依赖关系,方便的控制执行顺序;GCD不具备。
4、可以很方便的取消一个操作的执行;CGD不具备。
5、使用 KVO 观察对操作执行状态的更改;CGD不具备。

问题2:常驻线程有什么作用?

解释:
通常情况下,创建子线程,在里面执行任务,任务完成后,子线程会立刻销毁;如果需要经常子线程中操作任务,那么频繁的创建和销毁子线程会造成资源的浪费。
所以需要常驻线程。

二、GCD

主要结构如下:

2.1、同步/异步、串行/并发

2.1.1、同步串行

问题3:主队列同步

图2 主队列同步

解释:
队列引起的循环等待,不是线程引起的循环等待。
详细解释如下:

图3 同步串行
1、ios中默认会有一个主队列、主线程。
2、主队列是一个串行队列。
3、viewDidLoad在主队列中,可以看成一个任务1。
4、Block相当于在主队列添加任务2。
5、viewDidLoad在主线程运行。
6、dispatch_sync说明任务2也在主线程运行。
7、任务1完成后才能执行任务2。
但是任务1还没有完成,就开始执行任务2,任务2有依赖任务1的完成,任务1依赖任务2的完成,造成死锁

问题4:主队列异步

没有问题

解释:
虽然没有问题,但是主队列提交的任务,无论通过同步/异步方式,都要在主线程进行处理!!!

问题5:串行队列同步

图4 串行队列同步

解释:
不会有问题。
详细解释如下:

图3 串行队列

1、iOS默认会有一个主队列、主线程。
2、主队列是一个串行队列。
3、viewDidLoad在主队列中,可以看成一个任务1。
4、Block是在另一个串行队列中,可以看成任务2。
5、viewDidLoad在主线程运行。
6、dispatch_sync说明任务2也在主线程运行。
7、因为二者不是在同一个队列,不会存在死锁,但是任务2会延迟任务1执行。

2.2.2、同步并发

问题6:下面代码输出结果是:

图4 同步并发

解释
1、iOS默认会有一个主队列、主线程。
2、主队列是一个串行队列。
3、viewDidLoad在主队列中,可以看成一个任务1。
4、global_queue是全局并发队列,里面有任务2和任务3
5、viewDidLoad在主线程运行。
6、dispatch_sync说明global_queue中的任务也在主线程运行(会阻断线程,强制执行自己的)。
7、因为global_queue和主线程队列不是同一个队列,不会造成死锁。
8、因为global_queue是全局并发队列,一个任务不用管前面的任务是否执行完毕。所以任务2未完成时,可以执行任务3,然后执行任务2,都是在主线程执行。

2.2.3、异步串行

图5 异步串行

这段代码是经常使用的
代码分析:
1、ios中默认会有一个主队列、主线程。
2、主队列是一个串行队列。
3、viewDidLoad在主队列中,可以看成一个任务1。
4、Block相当于在主队列添加任务2。
5、viewDidLoad在主线程运行。
6、dispatch_async说明任务2在子线程运行,也就是不会阻挡任务1的运行。
7、任务1完成后才能执行任务2。
因为任务1在子线程运行,不会阻挡任务2,所以正常使用。

2.2.4、异步并发

问题7:以下代码输出结果:

图6 异步并发

解释
1、global_queue是全局队列,采用dispatch_async,所以会开辟一个子线程。
2、子线程的runLoop默认是不开启的,而performSelector:withObject:afterDelay是在没有runloop的情况下会失效,所以此方法不执行。
3、打印结果13。

2.3、dispatch_barrier_async()

2.3.1、场景

问题8:怎样利用CGD实现多读单写?

利用CGD提供的栅栏函数
解析:

多读单写模型

可以理解为:
1、读处理之间是并发的,肯定要用并发队列
因为读取操作,往往需要立刻返回结果,故采用同步
这些读处理允许在多个子线程。
2、写处理时候,其余操作都不能执行。利用栅栏函数,异步操作。利用栅栏函数异步操作的原因:栅栏函数同步操作会阻塞当前线程,如果当前线程还有其它操作,则会影响用户体验。

核心代码如下:

@interface UserCenter()
{
    // 定义一个并发队列
    dispatch_queue_t concurrent_queue;
    
    // 用户数据中心, 可能多个线程需要数据访问
    NSMutableDictionary *userCenterDic;
}

@end

// 多读单写模型
@implementation UserCenter

- (id)init
{
    self = [super init];
    if (self) {
        // 通过宏定义 DISPATCH_QUEUE_CONCURRENT 创建一个并发队列
        concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
        // 创建数据容器
        userCenterDic = [NSMutableDictionary dictionary];
    }
    
    return self;
}

- (id)objectForKey:(NSString *)key
{
    __block id obj;
    // 同步读取指定数据
    dispatch_sync(concurrent_queue, ^{
        obj = [userCenterDic objectForKey:key];
    });
    return obj;
}

- (void)setObject:(id)obj forKey:(NSString *)key
{
    // 异步栅栏调用设置数据
    dispatch_barrier_async(concurrent_queue, ^{
        [userCenterDic setObject:obj forKey:key];
    });
}

2.3.2、dispatch_barrier_sync和dispatch_barrier_async区别

共同点:

不同点:

注意:
使用栅栏函数时,使用自定义队列才有意义,如果使用串行队列/系统的全局并发队列,这个栅栏函数就相当于一个同步函数

2.3、dispatch_group

问题9:使用CGD实现这个需求:A、B、C三个任务并发,完成后执行任务D。

 // 创建一个group
 dispatch_group_t group = dispatch_group_create();
 // 异步组分派到并发队列当中
  dispatch_group_async(group, concurrent_queue, ^{
  });
  //监听  
  dispatch_group_notify(group, dispatch_get_main_queue(), ^{
      // 当添加到组中的所有任务执行完成之后会调用该Block
 });

三、NSOperation

3.1、NSOperation优点

问题10:我们可以控制任务的哪些状态?

3.2、状态控制

问题11:我们怎么控制NSOperation的状态

问题12:系统是怎样移除一个isFinished=YES的NSOperation的?

小结:
NSOperation: 主队列默认在主线程执行,自定义队列默认在后台执行(会开辟子线程)。

四、NSThread

4.1、启动流程

启动流程

1、调用start()方法、启动线程。
2、在start()内部会创建一个pthread线程,指定pthread线程的启动函数。
3、在启动函数中会调用NSThread定义的main()函数。
4、在main()函数中会调用performSelector:函数,来执行我们创建的函数。
5、指定函数运行完成,会调用exit()函数,退出线程。

4.2、常驻线程

参考RunLoop

五、多线程与锁

问题13:iOS中都有哪些锁,你是怎样使用的?

解释:

5.1、@synchronized(互斥锁) 🌟🌟🌟

5.2、 atomic(自旋锁)🌟🌟🌟

原子操作:不会被线程调度打断的操作;这种操作一旦开始,就一直运行到结束,中间不会切换到另一个线程。

不负责使用:属性赋值时候,能够保证线程安全;对属性进行操作,不能保证线程安全。
例如:
@property (atomic) NSMutableArray *array;
self.array = [NSMutableArray array];//线程安全
[self.array addObject:obj];//线程不安全

5.3、 OSSpinLock(自旋锁)

5.4、 NSLock(互斥锁)

蚂蚁金服面试题:

NSLock面试题
解释:
对同一把锁两次调用,由于重入的原因会造成死锁;解决办法就是使用递归锁(可以重入)。

5.5、 NSRecursiveLock(递归锁)(互斥锁)

NSRecursiveLock锁

5.6、 dispatch_semaphore_t(信号量)🌟🌟🌟

5.6.1、dispatch_semaphore_create
dispatch_semaphore_create
5.6.2、dispatch_semaphore_wait
dispatch_semaphore_wait
5.6.3、dispatch_semaphore_signal
dispatch_semaphore_signal

小结:
1、锁分为互斥锁自旋锁
2、互斥锁和自旋锁的区别
自旋锁: 忙等待。即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放
互斥锁: 会休眠。即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其它线程工作,直到被锁资源释放,此时会唤醒休眠线程。

问题14:iOS系统为我们提供了几种多线程技术?各自有什么特点?

解释:

总结:

概念理解

同步异步、串行并行形象理解

这两对概念单独看起来,明白怎么回事;但是,一旦运用起来,总是不能得心应手。总的来说,就是不能将概念熟记于心,缺乏形象概念。

下面采用图解 + 文字进行表述:

串行队列图解 并行队列图解

一个队列(串行+并发)好比一个容器
执行代码好比一个个任务
同步异步好比任务的标签
容器里面装有好多个打有标签任务
线程好比流水线的传送带
所有的工作都是CPU在做,姑且将CPU比做操作工

代码运行的时候,大家想象工厂的流水线的工作场景:
1、从容器(队列)中取出任务(执行代码),放到传送带上。
如果容器是串行队列,则完成一个,取出一个。
如果容器是并发队列,则一直不停的投放。
2、任务(执行代码)放到传送带(线程)的一刹那,CPU(操作工)看了一眼上面的标签:如果标签是同步,就将它放到当前传送带;如果标签是异步,就新增加一条传送带,然后把任务放上去(理解操作工无所不能,可以随意增加传送带)。

上面只是一个形象的比喻,加深对多线程理解。

小结:
从上面的分析可知:
1、串行队列任务之间相互包含,容易造成死锁;并发队列则不会。这种死锁称为队列死锁。
2、并发队列+异步,才会有多线程效果。
如果只有当前一个线程可以利用,并发队列中任务虽然可以快速取出分派,奈何只有一个线程(主干道),只能一个个排队执行。

上一篇下一篇

猜你喜欢

热点阅读