美文网首页iOS面试剖析
多线程相关知识点

多线程相关知识点

作者: huoshe2019 | 来源:发表于2019-10-08 10:59 被阅读0次
    图1 知识结构

    一、知识结构分析

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

    多线程之间的关系

    • pthread是POSIX线程的API

    • NSThread是Cocoa对pthread的封装

    • GCD和NSOperationQueue是基于队列的并发API

    • GCD是基于pthread和queue实现的

    • NSOperationQueue是对GCD的高级封装

    • GCD

    • NSOperation
      AFNetWorking + SDWebImage框架多有使用

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

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

    • NSThread
      用于实现一个常驻线程

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

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

    • 多线程和锁
      线程同步和资源共享

    二、GCD

    主要结构如下:

    • 同步/异步、串行/并发
    • dispatch_barrier_async
    • dispatch_group

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

    • dispatch_sync(serial_queue,^{ //任务 });

    • dispatch_async(serial_queue,^{ //任务 });

    • dispatch_sync(concurrent_queue,^{ //任务 })

    • dispatch_async(concurrent_queue,^{ //任务 })

    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区别

    共同点:

    • 它们前面的任务先执行完。
    • 它们的任务执行完,再执行后面的任务。

    不同点:

    • 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优点

    • 添加任务依赖
    • 任务执行状态控制
    • 最大并发量(maxConcurrentOperationCount)

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

    • isReady
      当前任务是否处于就绪状态
    • isExecuting
      当前任务是否正在执行
    • isFinished
      当前任务是否执行完成
    • isCancelled
      当前任务是否被标记为取消(不是判断是否被取消,是标记)
      是通过KVO进行控制的

    3.2、状态控制

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

    • 如果只重写了main方法,底层控制任务执行状态以及任务退出。
    • 如果重写了start方法,自行控制任务状态。

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

    • 通过KVO

    小结:
    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(自旋锁)

    • 循环等待访问,不释放当前资源。
    • 用于轻量级数据访问,简单的int值 +1/-1操作。
    • 使用场景:
      • 内存引用计数加1或减1
      • runtime也有使用到。

    5.4、 NSLock(互斥锁)

    蚂蚁金服面试题:

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

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

    NSRecursiveLock锁

    5.6、 dispatch_semaphore_t(信号量)🌟🌟🌟

    • dispatch_semaphore_create(5)
      创建信号量,指定最大并发数
    • dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
      等待信号量>=1。
      如果当前信号量>=1,信号量减1,程序继续执行。
      如果信号量<=0,原地等待,不允许程序往下执行。
    • dispatch_semaphore_signal(semaphore)
      程序执行完毕,发送信号,释放资源,信号量加1。
    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系统为我们提供了几种多线程技术?各自有什么特点?

    解释:

    • GCD
      用于一些简单的线程同步,包括子线程分派;还有就是解决类似于多读单写功能。

    • NSOperation及NSOperationQueue
      可以方便控制任务状态、添加依赖/移除依赖;多用于复杂线程控制:AFNetWorking和SDWebImage。

    • NSThread
      往往用于实现一个常驻线程。

    总结:

    概念理解

    • 队列概念
      队列是任务的容器

    • 串行队列、并发队列区别
      串行队列: 一次仅仅调度一个任务,队列中的任务一个个执行。(一个任务完成后,再运行下一个任务)。
      遵循FIFO原则:先进先出、后进后出

      并发队列:不需要把一个任务完成后,再运行下一个任务。
      仍然遵循FIFO原则,只是不需要等待任务完成。

    • 并行、并发区别
      并行:同一时刻,多条指令在多个处理器同时执行。
      并发:同一个时刻,只能处理一条指令,但是多个指令被快速的轮换执行,达到了具有同时执行的效果。

    • 异步、同步区别
      异步: 可以开启新的线程。
      同步: 不可以开启新的线程,在当前线程运行。

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

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

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

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

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

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

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

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

    相关文章

      网友评论

        本文标题:多线程相关知识点

        本文链接:https://www.haomeiwen.com/subject/bwgzyctx.html