OC面试题 五、多线程

482 阅读5分钟

iOS中的常见多线程方案

image.png

GCD的队列

GCD的队列可以分为2大类型

  1. 并发队列(Concurrent Dispatch Queue)
  • 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
  • 并发功能只有在异步(dispatch_async)函数下才有效
  1. 串行队列(Serial Dispatch Queue)
  • 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)

各种队列的执行效果

image.png

队列组的使用

  1. 思考:如何用gcd实现以下功能
  • 异步并发执行任务1、任务2
  • 等任务1、任务2都执行完毕后,再回到主线程执行任务3

image.png

有哪些锁?

  • OSSpinLock
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock
  • @synchronized

dispatch_semaphore

  • semaphore叫做”信号量”
  • 信号量的初始值,可以用来控制线程并发访问的最大数量
  • 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

image.png

自旋锁、互斥锁比较

  1. 什么情况使用自旋锁比较划算?
  • 预计线程等待锁的时间很短
  • 加锁的代码(临界区)经常被调用,但竞争情况很少发生
  • CPU资源不紧张
  • 多核处理器
  1. 什么情况使用互斥锁比较划算?
  • 预计线程等待锁的时间较长
  • 单核处理器
  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈

多读单写实现方案

  • pthread_rwlock: 读写锁
  • dispatch_barrier_async:异步栅栏调用

是否可以在子线程刷新UI?

不可以。因为子线程代码执行完毕了,又自动进入到了主线程,执行了子线程中的UI更新的函数栈,这中间的时间非常的短,就让大家误以为线程可以更新UI。如果子线程一直在运行,则子线程中的UI更新的函数栈,主线程无法获知,即无法更新

viewDidLoad dispatch_sync 主队列同步执行问题

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2");
    });
    NSLog(@"3");
}

image.png 结果只会打印1,然后线程卡死,这是为什么呢?

首先执行任务1,这是肯定没问题的,只是接下来,程序遇到了同步线程,那么它会进入等待,等待任务2执行完,然后执行任务3。但这是队列,有任务来,当然会将任务加到队尾,然后遵循FIFO原则执行任务。那么,现在任务2就会被追加到最后,任务3排在了任务2前面,问题来了:任务3要任务2执行完才能执行,任务2又排在任务3后面,意味着任务2要在任务3执行完才能执行,所以他们进入互相等待的局面。这就是死锁

优化方案:

NSLog(@"1");
    ///获取一个全局队列
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@"2");
    });
    NSLog(@"3");

GCD实现多个接口请求完成后刷新UI

///GCD多个接口请求完之后刷新UI
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("zz.zg", DISPATCH_QUEUE_SERIAL);
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{

        ///网络请求1

        ///成功失败,离开组

        dispatch_group_leave(group);

    });

    dispatch_group_enter(group);

    dispatch_group_async(group, queue, ^{

        ///网络请求2

        ///成功失败,离开组

        dispatch_group_leave(group);

    });

    dispatch_group_enter(group);

    dispatch_group_async(group, queue, ^{

        ///网络请求3

        ///成功失败,离开组

        dispatch_group_leave(group);

    });

    dispatch_group_enter(group);

    dispatch_group_async(group, queue, ^{

        ///网络请求4

        ///成功失败,离开组

        dispatch_group_leave(group);

    });

    ///4个网络请求结束,更新UI
    dispatch_group_notify(group, queue, ^{

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            dispatch_async(dispatch_get_main_queue(), ^{

                ///更新UI

            });

        });

    });

app运行过程中,同时最多有几个线程,怎么实现的高并发?

同时最多有几个线程: 根据cpu的能力,目测:50个
生活中遇到的很多场景,多是IO密集型。解决这类问题的核心思想就是减少cpu空转的时间,增加CPU的利用率。具体有下面两种方法:
1. 限制活动线程的个数不超过硬件线程的个数

  • 活动线程指Runnable状态的线程。
  • Blocked状态的线程个数不在限制内。Blocked状态的线程都在等待外部事件触发,比如鼠标点击、磁盘IO操作事件,操作系统会将他们移除到调度队列外,所以它们不会消耗cpu时间片。程序可以有很多的线程,但是只要保证活动的线程个数小于硬件线程的个数,运行效率是可以保证的
  • 计算密集型和IO密集型线程是要分开看待的。计算密集型线程应该永远不被block,大部分时间都要保证runnable状态。要有效的利用处理器资源,可以让计算密集型的线程个数跟处理器个数匹配。而IO密集型线程大部分时间都在等待IO事件,不需要太多的线程。

2. 基于任务的编程(协程)

线程个数跟硬件线程一致。任务调度器把对应的任务放入跟线程做一个映射,放入到相应的线程执行。有几个明显的优势:

  • 按需调度。线程调度器的时间片是公平的分配给各个线程的,因为它不理解程序的业务逻辑。这就跟计划经济样的,极度的公平就是不公平,市场经济这种按需分配才能提高效率。任务调度器是理解任务信息的,可以更有效的调度任务。
  • 负载均衡。将程序分成一个个小的任务,让调度器来调度,不让所有的线程空跑,保证线程随时有活干。有效的利用计算资源、平衡计算资源。
  • 更易编程。以线程为基础的编程,要提高效率,经常要考虑到底层的硬件线程,考虑线程调度受到的影响。但是如果基于任务来编程,只要集中注意力在任务之间的逻辑关系上,处理好任务之间的关系。调度效率可以交给调度器来管控。