iOS 底层学习22 —多线程

前言

iOS 底层第22天的学习。今天又要迎来一个新的篇章的学习。这个篇章就是多线程

多线程引入

  • 说到线程,肯定要说到另一个词 — 进程

进程的基本概念

进程: 是指在系统中正在运行的一个程序
每个进程之间是独⽴的,每个进程均运⾏在其专⽤的且受保护的内存空间内
通过“活动监视器”可以查看 Mac 系统中所开启的进程

线程的基本概念

线程是进程的基本执⾏单元,⼀个进程的所有任务都在线程中执⾏
进程要想执⾏任务,必须得有线程,进程⾄少要有⼀条线程
程序启动会默认开启⼀条线程,这条线程被称为主线程或 UI 线程

翻译👇

线程 是应用程序内实现多个执行路径的一种相对轻量级的方式。在系统层面,程序并排运行,系统根据其需求和其他程序的需求为每个程序提供执行时间。然而,在每个程序中,存在一个或多个执行线程,这些线程可用于同时或几乎同时执行不同的任务。系统本身实际上管理这些执行线程,安排它们在可用核心上运行,并根据需要先发制人地中断它们,以允许其他线程运行。

线程与进程的关系

地址空间:同⼀进程的线程共享本进程的地址空间,⽽进程之间则是独⽴的地址空间。资源拥有:同⼀进程内的线程共享本进程的资源如内存I/Ocpu 等,但是进程之间的资源是独⽴的。
资源拥有:同⼀进程内的线程共享本进程的资源如内存I/Ocpu 等,但是进程之间的资源是独⽴的。

1: ⼀个进程崩溃后,在保护模式下不会对其他进程产⽣影响,但是⼀个线程崩溃整个进程都死掉。所以多进程要⽐多线程健壮。
2: 进程切换时,消耗的资源⼤,效率⾼。所以涉及到频繁的切换时,使⽤线程要好于进程。同样如果要求同时进⾏并且⼜要共享某些变量的并发操作,只能⽤线程不能⽤进程
3: 执⾏过程:每个独⽴的进程有⼀个程序运⾏的⼊⼝、顺序执⾏序列和程序⼊⼝。但是线程不能独⽴执⾏,必须依存在应⽤程序中,由应⽤程序提供多个线程执⾏控制。
4: 线程是处理器调度的基本单位,但是进程不是。
5: 线程没有地址空间,线程包含在进程地址空间中

线程成本

  • 看下官方的这幅图👇
项目 大约成本 备注
内核数据结构 大约1KB 此内存用于存储线程数据结构和属性,其中大部分被分配为有线内存,因此无法分页到磁盘
堆栈空间 512 KB(二次线程)8 MB(OS X 主线程)1 MB(iOS主线程) 辅助线程允许的最小堆栈大小为16KB,堆栈大小必须是4KB的倍数。在线程创建时,此内存的空间将放在您的进程空间中,但在需要之前,才会创建与该内存关联的实际页面。
创建时间 大约90微秒 此值反映了从创建线程的初始调用到线程的入口点例程开始执行的时间。这些数字是通过分析基于英特尔的iMac上线程创建时生成的平均值和中位数确定的,iMac具有2GHz Core Duo处理器和运行OS X v10.5的1GB内存。

多线程意义

  • 看一下官方文档对多线程的解释

翻译👇

在应用程序中具有多个线程提供了两个非常重要的潜在优势:

  • 多个线程可以提高应用程序的感知响应能力。
  • 多线程可以提高应用程序在多核系统上的实时性能。
    如果您的应用程序只有一个线程,则该线程必须完成所有工作。它必须响应事件,更新应用程序的窗口,并执行实现应用程序行为所需的所有计算。只有一个线程的问题在于它一次只能做一件事。那么,当您的计算需要很长时间才能完成时会发生什么?当您的代码忙于计算所需的值时,应用程序会停止响应用户事件并更新其窗口。如果这种行为持续足够长的时间,用户可能会认为您的应用程序被挂起,并试图强制退出它。但是,如果您将自定义计算移到一个单独的线程上,应用程序的主线程将可以更及时地响应用户交互。
  • 优点
    • 能适当提⾼程序的执⾏效率
    • 能适当提⾼资源的利⽤率(CPU,内存)
    • 线程上的任务执⾏完成后,线程会⾃动销毁
  • 缺点
    • 开启线程需要占⽤⼀定的内存空间(默认情况下,每⼀个线程都占512KB) 如果开启⼤量的线程,会占⽤⼤量的内存空间,降低程序的性能
    • 线程越多,CPU在调⽤线程上的开销就越⼤
    • 程序设计更加复杂,⽐如线程间的通信、多线程的数据共享

多线程原理

  • 多线程并非真正意义上的并发,只是因为 CPU 在同一个时间点上调度线程的速度非常快,所以宏观上就近似并行多线程了。
  • 时间⽚的概念:CPU在多个任务直接进⾏快速的切换,这个时间间隔就是时间⽚
  • 单核CPU
    • 同⼀时间,CPU只能处理1个线程, 换⾔之,同⼀时间只有1个线程在执⾏
  • 多线程同时执⾏:
    • CPU快速的在多个线程之间的切换,CPU 调度线程的时间⾜够快,就造成了多线程的“同时”执⾏的效果
  • 如果线程数⾮常多
    • CPU 会在 N 个线程之间切换,消耗⼤量的 CPU 资源,每个线程被调度的次数会降低,线程的执⾏效率降低

多线程技术方案

方案 简介 语言 线程生命周期 使用频率
pthread 一套通用 API ,使用难度大,跨平台,可移值 C 手动管理 几乎不用
NSThread 使用更加面向对象,简单易用,可直接操作对象 OC 手动管理 偶尔使用
GCD 旨在替代 NSThread 等技术,充分利用设备多核 C 自动管理 经常使用
NSOperation 底层基于 GCD,使用更加面向对象 OC 自动管理 经常使用

线程生命周期

  • 代码👇
  // 创建一个线程 New
  NSThread *t = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
  // start 
  [t start]; // 进入 runable
  
// 对 run 的实现
- (void)run{
    NSLog(@"开始");
    //下面的代码的作用是判断线程状态,可能因为下面的延时,阻塞会带来当前线程的一些影响
    // sleep 模拟线程堵塞 Blocked
   [NSThread sleepForTimeInterval:3];

  for (NSInteger i = 0; i < 10; i++) {
        // 判断线程是否被取消
        if ([NSThread currentThread].isCancelled) {
            NSLog(@"%@被取消",self.t.name);
            return;
        }
        // 开始 running 
        NSLog(@"%@ %zd", [NSThread currentThread], i);
        //内部取消线程
        // 强制退出 - 当某一个条件满足,不希望线程继续工作,直接杀死线程,退出
        // 线程 Dead
        if (i == 8) {
            // 强制退出当前所在线程!后续的所有代码都不会执行
            [NSThread exit];
        }
    }
}
  • 在线程生命周期里有个东西叫 线程池
  • 那线程池是用来干什么的呢? 顾名思义 就是存放线程的嘛
  • 那线程池调度线程的具体流程是什么样的呢? 请看👇


  • 饱和策略如何处理
    • AbortPolicy: 直接抛出RejectedExecutionExeception异常来阻⽌系统正常运⾏
    • CallerRunsPolicy: 将任务回退到调⽤者
    • DisOldestPolicy: 丢掉等待最久的任务
    • DisCardPolicy: 直接丢弃任务

线程锁

自旋锁 vs 互斥锁

  • 自旋锁 : 发现其他线程执行 当前线程 询问 - 忙等 耗费性能比较高,一般用在大端
  • 互斥锁: 发现其他线程执行 当前线程 休眠 (就绪状态) 一直在等打开 唤醒执行.下面会重点讲解

互斥锁

  • 保证锁内的代码,同⼀时间,只有⼀条线程能够执⾏!
  • 互斥锁的锁定范围,应该尽量⼩,锁定范围越⼤,效率越差!
  • 互斥锁参数
    • 能够加锁的任意 NSObject 对象
    • 注意:锁对象⼀定要保证所有的线程都能够访问
    • 如果代码中只有⼀个地⽅需要加锁,⼤多都使⽤ self,这样可以避免单独再创建⼀个锁对象

多线程知识点补充

  1. 任务执行速度的影响因素有哪些?
  • 1.CPU (单核 or 多核)
  • 2.任务的复杂度
  • 3.优先级的设定
  • 4.线程状态
  1. 优先级翻转
    优先级翻转是当一个高优先级任务通过信号量机制访问共享资源时,该信号量已被一低优先级任务占有,因此造成高优先级任务被许多具有较低优先级任务阻塞,实时性难以得到保证。
  • 那为什么高优先级任务会被较低优先级任务阻塞?
    我们来解释这下面这2个东西
IO 密集型

IO 密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。

CPU 密集型

CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是 CPU Loading 100%CPU 要读/写I/O(硬盘/内存),I/O 在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading 很高。
具体概念可参考这篇文章

小结一下:

IO 密集型 -> 线程任务频繁等待
CPU 密集型 -> 线程任务很少等待
因此就会导致 IO 密集型的线程 被饿死,这时 CPU 开始进行调度把 IO 密集型的任务的优先级临时的给提高上去 -> 线程被执行的可能性提高, 就导致了 优先级翻转

优先级因素
  1. 用户指定
  2. 等待的频繁的
  3. 任务长时间不执行
  1. atomic 与 nonatomic 的区别
atomic

是原子属性,是为多线程开发准备的,是默认属性!
仅仅在属性的 setter 方法中,增加了锁(自旋锁),能够保证同一时间,只有一条线程对属性进行操作 , 同一时间 单(线程)写多(线程)读的线程处理技术。线程安全,需要消耗⼤量的资源

  • 底层源码👇
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    // ..... 
   if (!atomic) {
       oldValue = *slot;
       *slot = newValue;
   } else {
     // 添加一个自旋锁
       spinlock_t& slotlock = PropertyLocks[slot]; 、
       slotlock.lock();
       oldValue = *slot;
       *slot = newValue;        
       slotlock.unlock();
   }
  • 从源码可知 atomic 只是一个判断的标识
nonatomic

是非原子属性
没有锁!性能高!⾮线程安全,适合内存⼩的移动设

  • 小结: iOS 开发建议
    所有属性都声明为 nonatomic
    尽量避免多线程抢夺同⼀块资源
    尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减⼩移动客户端的压力

总结

  • 今天分享的多线程篇章概念性的东西比较多:
    • 什么线程,线程和进程区别
    • 多线程的原理:它并非真正意义上的并发,只是因为 CPU 在同一个时间点上调度线程的速度非常快,所以宏观上就近似并行多线程了
    • 线程生命周期 从 new -> start -> runable -> running -> blocked -> Dead
    • 最后也补充几个问题,扩展了知识面。
    • 预告:下次要学习的就是iOS多线程里用的最多的 GCD 底层的探索
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容