美文网首页
22.iOS底层学习之多线程原理

22.iOS底层学习之多线程原理

作者: 牛牛大王奥利给 | 来源:发表于2021-12-30 12:35 被阅读0次

    本篇提纲:
    1、线程与进程
    2、多线程
    3、多线程相关面试题
    4、线程安全问题
    5、线程与runloop的关系

    线程与进程
    • 进程
      进程是指可以在操作系统中独立运行并且作为资源分配的基本单位;
      例如正在运行的一个应用程序,我们可以理解为一个进程
      进程进程之间是相互独立的,每个进程都运行在其专用的且受保护的内存空间;

    • 线程
      线程是操作系统中作为调度和分派的基本单位;
      线程是进程的基本执行单元,一个进程的所有任务都在线程中执行;
      程序启动会默认开启一条线程,这条线程我们称为主线程或者UI线程

    • 进程与线程的关系
      1、地址空间:同一个进程的线程共享本进程的地址空间,而多进程的地址空间是相互独立的。
      2、资源拥有:同一进程的线程共享本进程的资源,如:内存、IO、CPU等,而多进程之间的资源是独立的。

    他们之间的关系可以用不同的公司和生产线来比喻,比如我们现在有很多正在运作的公司,每个公司我们可以理解为一个进程;而每个公司的内部有很多部门,比如产品部、市场部、技术部去生产不同的东西,可以理解为公司内部的三条线程,各自有各自的任务,但是整个公司的空间和资源是这三个线程可以共享的。

    多进程要比多线程更加健壮,一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃了会导致整个进程都崩溃掉。

    我看到了一篇解释进程和线程关系的比较清楚易懂的文章,还有疑问可以看下进程和线程关系的简单解释

    多线程

    多线程的实现原理:
    iOS中的多线程,由CPU在多个任务之间进行快速切换,CPU调度线程的时间足够快,就造成了多线程的“同时”执行的效果。

    时间片:CPU在多个任务之间进行快速的切换,这个时间间隔称为时间片

    如果开启的线程太多会导致的问题:
    1、CPU会在N个线程之间来回切换,会消耗大量的CPU资源。
    2、每个线程的调度次数会被降低,线程的执行效率会很低。

    多线程的好处:
    1、能适当提高程序的执行效率。
    2、能适当的提高CPU、内存等资源的利用率。

    多线程会带来的问题:
    1、开启线程需要占用内存,默认情况下,每一个线程都占512KB。
    2、会让程序设计更加复杂,比如线程之间的通信,多线程的资源共享问题。

    iOS多线程技术方案:

    方案 简介 语言 线程生命周期 使用频率
    pthread - 一套通用的多线程API
    - 适用于Unix/Linux/Windows等系统
    - 跨平台、可移植
    - 使用难度大
    C语言 程序员管理 几乎不用
    NSThread - 面向对象
    - 简单易用,可直接操作线程对象
    OC语言 程序员管理 偶尔使用
    GCD - 旨在替代NSThread等线程技术
    - 充分利用设备多核
    C语言 自动管理 经常使用
    NSOperation - 基于GCD(底层是GCD)
    - 比GCD多了一些更简单实用的功能
    -使用更加面向对象
    OC语言 自动管理 经常使用

    多线程生命周期:
    线程的生命周期主要分为五个阶段:

    • 新建
      实例化线程对象

    • 就绪
      线程对象调用start方法,将线程对象加入到可调度的线程池,等待系统分配CPU进行调度。调用start方法并不会立即执行,而是进入到就绪状态,之后CPU调度该线程,才会进入运行状态。

    • 运行
      CPU负责调度可调度线程池中线程的执行,直到因为等待资源被阻塞或者执行完任务而死亡,在运行期间,其状态可能会在就绪和运行之间进行切换,这个变化由CPU负责,分配相应的时间片给线程。

    • 阻塞
      处于运行状态的某些线程,在某种情况下,比如执行了sleep,或者等待I/O资源等情况,线程将让出CPU暂停自己的运行,进入阻塞状态。
      当阻塞的原因消除时,被阻塞的线程会重新回到就绪队列,回到就绪状态。

    • 死亡
      死亡大致分为三种情况:
      1、线程执行任务完毕,正常死亡;
      2、线程被强制终止,比如通过stop方法终止线程;
      3、线程抛出未捕获的异常;

    线程生命周期图.jpg

    线程池原理:
    1、先去判断核心线程池是否都在执行任务

    • 返回NO,创建新的任务,让线程去执行
    • 返回YES,进入2

    2、判断线程池工作队列是否饱满

    • 返回NO,将任务存储到工作队列,等待CPU调度
    • 返回YES,进入3

    3、判断线程池中的线程是否都处于执行状态

    • 返回NO,安排可调度线程池中的空闲线程去执行
    • 返回YES,进入4

    4、交给饱和策略去执行,分为以下四种拒绝策略:

    • AbortPolicy:直接抛出RejectedExecutionExeception异常,阻止系统正常运行
    • CallerRunsPolicy:将任务回退到调用者
    • DisOldestPolicy:丢掉等待最久的任务
    • DisCardPolicy:直接丢掉任务

    四种拒绝策略均实现的RejectedExecutionHandler接口

    多线程相关面试题
    • 任务执行速度的影响因素
      1、CPU的调度情况
      2、任务的复杂度
      3、任务的优先级
      4、线程的状态

    • 优先级调度
      线程的优先级不仅可以由用户手动设置,系统还会根据不同的线程的表现自动调整优先级。通常情况下频繁的进入等待状态的线程比频繁进行大量计算,以至于每次都要把时间片全部用尽的线程要受欢迎。频繁等待的线程通常占用很少的时间。

    • IO密集型线程(IO Bound Thread):频繁等待的线程

    • CPU密集型线程(CPU Bound Thread):很少等待的线程。

    IO密集型比CPU密集型更容易得到线程优先级的提升

    IO操作的速度很慢,并且需要频繁的等待,如果它的优先级又低很容易被饱和策略所淘汰。为了避免这种情况,当CPU发现一个频繁等待的线程,会提升它的优先级,从而提升线程被执行的可能性。

    • 优先级的影响因素
      1、用户指定优先级。例如QualityOfService
      2、 根据进入等待状态的频繁程度提升或降低优先级。
      3、长时间得不到执行而被提升优先级。
    线程安全问题
    • 互斥锁
      同一时间只有一条线程能执行,当发现有其他线程正在执行,当前线程会进入休眠状态,等待被唤醒。

    使用注意事项:
    互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差。
    能够加锁的任意NSObject对象
    锁对象一定要保证所有的线程都能够访问

    • 自旋锁
      自旋锁与互斥锁类似,但是它不是让线程处于休眠状态,而是在没有获取锁之前不放弃当前的时间片,一直处于一个忙等的状态,一直询问“到我没,到我没”这样一个状态。

    自旋锁的使用场景:锁持有的时间短,或者线程不希望在重新调度上花太多成本时,就可以使用自旋锁。

    使用自旋锁,当新的线程访问代码时,如果发现有其他线程已经锁定了代码,新线程会用一种死循环的方法,一直等待锁定的代码执行完成,即不停的尝试执行代码,比较消耗性能。

    附一张网上查到的自旋锁和非自旋锁的流程图:


    自旋锁和非自旋.png

    自旋锁的好处:
    自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。

    因为阻塞和唤醒线程都是需要高昂的开销的,如果同步代码块中的内容不复杂,那么可能切换线程带来的开销比实际业务代码执行的开销还要大。

    在很多场景下,可能我们的同步代码块的内容并不多,所以需要的执行时间也很短,如果我们仅仅为了这点时间就去切换线程状态,那么其实不如让线程不切换状态,而是让它自旋地尝试获取锁,等待其他线程释放锁,有时我只需要稍等一下,就可以避免上下文切换等开销,提高了效率。

    • atomic与nonatomic
      atomic:原子属性,针对多线程设计的,默认值。线程安全,需要消耗大量的资源。
      nonatomic:非原子属性,非线程安全,没有锁,性能高,移动端常用。

    关于atomic的源码分析,来看下内部的锁:


    image.png

    它的内部实现是添加的这种spinlock_t锁,而spinlock_t是:

    using spinlock_t = mutex_tt<LOCKDEBUG>;
    class mutex_tt : nocopy_t {
        os_unfair_lock mLock;
     public:
       .......
    }
    
    OS_UNFAIR_LOCK_AVAILABILITY
    typedef struct os_unfair_lock_s {
        uint32_t _os_unfair_lock_opaque;
    } os_unfair_lock, *os_unfair_lock_t;
    

    根据注释部分的描述,可以了解到os_unfair_lock加完锁之后,会让另外一个线程进入休眠状态,而不是忙等,所以其实是互斥锁,这个改变是在iOS10之后,在iOS10之前使用的是OSSpinLock,之所以被os_unfair_lock替代也是出于线程饿死的问题。

    image.png
    线程与RunLoop的关系

    1、RunLoop与线程是一一对应的,一个Runloop对应一个核心线程。为什么说是核心的,因为RunLoop里可以进行嵌套,但是核心只能有一个,他们的关系保存在一个全局字典里。

    2、RunLoop是来管理线程的,当线程的RunLoop被开启后,线程会在执行完任务进入休眠状态,有了任务就会唤醒去执行任务。

    3、RunLoop在第一次获取时被创建,在线程结束时被销毁。

    4、主线程的RunLoop,在程序启动的时候默认创建。

    5、对于子线程来说,RunLoop是懒加载的,只有当我们使用的时候才会去创建。所以,当子线程使用定时器时,要确保子线程的RunLoop被创建,不然定时器无法进行回调。

    相关文章

      网友评论

          本文标题:22.iOS底层学习之多线程原理

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