iOS底层原理探索-多线程原理

多线程在 iOS 中有着举足轻重的地位,合适地使用多线程不仅可以保证App的质量,也提高用户的体验度

本文的目的在于了解进程、线程、多线程等基本概念及原理

同样的,几个问题:

  • 线程是什么,进程是什么
  • 线程与进程的关系
  • 多线程原理
  • 常用的多线程方案

线程和进程

线程

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

进程

  • 进程是指在系统中正在运行的一个应用程序
  • 每个进程之间是相互独立的,每个进程均运行在其专用且受保护的内存空间

简单理解为

进程是线程的容器,而线程是用来执行任务

在 iOS 中是单进程开发,一个进程就是一个 App,进程之间是相互独立的,如支付宝、微信等,属于不同的一个进程

线程与进程的关系

进程与线程之间的关系,主要涉及两个方面:

  • 地址空间
    • 同一个进程的线程共享本进程的地址空间
    • 进程之间则是独立的地址空间
  • 资源拥有
    • 同一个进程内线程共享本进程的资源,如内存、I/O、CPU 等
    • 进程之间资源是相互独立的

两者的关系就如同工厂与流水线的关系,工厂与工厂之间是独立的,但工厂中的流水线是共享工厂的资源

除此之外,还有几点:

  • 多进程要比多线程健壮
    • 一个进程崩溃后,在保护模式下不会对其他进程产生影响
    • 一个线程崩溃后,整个进程都会死掉
  • 使用场景:频繁切换、并发操作
    • 进程切换时,消耗资源大,效率高。所以涉及到频繁切换时,使用线程要好于进程
    • 如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
  • 执行过程
    • 每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口
    • 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
  • 线程是处理器的基本单元,但是进程不是
  • 线程没有地址空间,线程包含在进程地址空间中

线程与进程的关系图

image-20201217214826984

线程和RunLoop的关系

  • runloop 与线程是一一对应的,一个 runloop 对应一个核心的线程

    因为 runloop 是可以嵌套的,但是核心的只能有一个,它们的关系保存在一个全局的字典里

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

  • runloop 在第一次获取时被创建,在线程结束时被消耗

    • 对于主线程来说,runloop 在程序启动时就默认创建好了
    • 对于子线程来说,runloop 是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的 runloop 被创建

多线程

多线程原理

  • 对于单核 CPU,同一时间,CPU只能处理一条线程,即只有一条线程在工作

  • 多线程并发执行,其实是 CPU 在多条线程之间快速地调度

多线程意义

  • 优点
    • 能适当提高程序的执行效率
    • 能适当提高资源的利用率,如 CPU、内存
    • 线程上的任务执行完成后,线程会自动销毁
  • 缺点
    • 开启线程需要占用一定的内存空间,默认情况下,每一条线程占用 512KB,创建线程大约需要 90ms 的创建时间
    • 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
    • 线程越多,CPU 在调用线程上的开销越大
    • 程序设计更加复杂,比如线程间的通信,多线程的数据共享

多线程生命周期

多线程的生命周期主要分为 5 部分:新建-就绪-运行-堵塞-死亡

image-20201217234042973

  • 新建:主要是实例化线程对象
  • 就绪:线程对象调用 start 方法,将线程对象加入可调度线程池,等等 CPU 的调用
  • 运行:CPU 负责调度可调度线程池中线程的执行。在线程执行完成任务之前,状态可能会在就绪和运行之间来回切换,这个变化是由 CPU 负责的,程序员不能干预
  • 阻塞:当满足某个预定条件时,可以使用休眠——sleep或同步锁,阻塞线程执行,当进入 sleep时,会重新将线程加入就绪状态
    • sleepUntilDate——阻塞当前线程,直到指定的时间为止,即休眠到指定时间
    • sleepForTimeInterval——在给定的时间间隔内休眠线程,即指定休眠时长
    • @synchronized()——同步锁
  • 死亡:分为两种情况
    • 正常死亡,线程执行完毕,自动销毁
    • 非正常死亡,在满足某个条件后,在线程内部终止执行/在主线程中止线程对象

线程的 exitcancel 说明

  • exit——一旦强行终止线程,后续的所有代码都不会执行
  • cancel——取消当前线程,但是不能取消正在执行的线程

线程的优先级越高,是不是意味着任务的执行越快?

答:并不是,影响线程执行速度的因素有以下几点:

- CPU 调度情况 - 任务的复杂度 - 任务的优先级 - 任务队列的情况

线程池原理

  • 线程池大小 小于 核心线程池 时,会直接去创建线程执行任务

  • 线程池大小 大于等于 核心线程池 时,则按下图的流程

    • 判断核心线程池是否都正在执行任务
      • 如果没有,那么会创建新的线程去执行任务
    • 判断线程池的工作队列是否已经饱满
      • 如果没有,会将任务加入到工作队列中,等待 CPU 调度
    • 判断线程池中的线程是否都处于执行状态
      • 如果没有,安排可调度线程池中空闲的线程去执行任务
    • 交给饱和策略去执行,主要有四种
      • AbortPolicy:直接抛出RejectedExecutionExeception异常来阻止系统正常运行
      • CallerRunsPolicy:将任务回退到调用者
      • DisOldestPolicy:丢掉等待最久的任务
      • DisCardPolicy:直接丢弃任务

iOS中多线程的实现方案

iOS 中多线程实现方式,主要有四种:pthread、NSThread、GCD、NSOperation

image-20201218010839215

简单实例:

// *********1: pthread*********
pthread_t threadId = NULL;
//c字符串
char *cString = "HelloCode";
/**
 pthread_create 创建线程
 参数:
 1. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾
 同时不需要 `*`
 2. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的)
 3. 线程要执行的`函数地址`
 void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
 (*): 函数名
 (void *): 参数类型,void *
 4. 传递给第三个参数(函数)的`参数`
 */
int result = pthread_create(&threadId, NULL, pthreadTest, cString);
if (result == 0) {
    NSLog(@"成功");
} else {
    NSLog(@"失败");
}

void *pthreadTest(void *para){
    // 接 C 语言的字符串
    //    NSLog(@"===> %@ %s", [NSThread currentThread], para);
    // __bridge 将 C 语言的类型桥接到 OC 的类型
    NSString *name = (__bridge NSString *)(para);
    
    NSLog(@"===>%@ %@", [NSThread currentThread], name);
    
    return NULL;
}

//*********2、NSThread*********
[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
    
//*********3、GCD*********
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self threadTest];
});
    
//*********4、NSOperation*********
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
    [self threadTest];
}];

- (void)threadTest{
    NSLog(@"begin");
    NSInteger count = 1000 * 100;
    for (NSInteger i = 0; i < count; i++) {
        // 栈区
        NSInteger num = i;
        // 常量区
        NSString *name = @"zhang";
        // 堆区
        NSString *myName = [NSString stringWithFormat:@"%@ - %zd", name, num];
        NSLog(@"%@", myName);
    }
    NSLog(@"over");
}

C 和 OC 的桥接

  • __bridge只做类型转换,但是不修改对象(内存)管理权
  • __bridge_retained(也可以使用CFBridgingRetain)将Objective-C的对象转换为 Core Foundation的对象,同时将对象(内存)的管理权交给我们,后续需要使用 CFRelease或者相关方法来释放对象
  • __bridge_transfer(也可以使用CFBridgingRelease)将Core Foundation的对象 转换为Objective-C的对象,同时将对象(内存)的管理权交给ARC

线程安全问题

当多线程同时访问一块资源时,容易引发数据错乱和数据安全的问题,常见的解决方案是通过

关于锁的分析,可以查看iOS底层原理探索-常见的锁分析

线程间通信

Threading Programming Guide文档中,提及,线程间的通讯有以下几种方式

image-20201218011421384

  • 直接消息传递: 通过performSelector的一系列方法,可以实现由某一线程指定在另外的线程上执行任务。因为任务的执行上下文是目标线程,这种方式发送的消息将会自动的被序列化
  • 全局变量、共享内存块和对象: 在两个线程之间传递信息的另一种简单方法是使用全局变量,共享对象或共享内存块。尽管共享变量既快速又简单,但是它们比直接消息传递更脆弱。必须使用锁或其他同步机制仔细保护共享变量,以确保代码的正确性。 否则可能会导致竞争状况,数据损坏或崩溃。
  • 条件执行: 条件是一种同步工具,可用于控制线程何时执行代码的特定部分。您可以将条件视为关守,让线程仅在满足指定条件时运行。
  • Runloop sources: 一个自定义的 Runloop source 配置可以让一个线程上收到特定的应用程序消息。由于 Runloop source 是事件驱动的,因此在无事可做时,线程会自动进入睡眠状态,从而提高了线程的效率
  • Ports and sockets:基于端口的通信是在两个线程之间进行通信的一种更为复杂的方法,但它也是一种非常可靠的技术。更重要的是,端口和套接字可用于与外部实体(例如其他进程和服务)进行通信。为了提高效率,使用 Runloop source 来实现端口,因此当端口上没有数据等待时,线程将进入睡眠状态。需要注意的是,端口通讯需要将端口加入到主线程的Runloop中,否则不会走到端口回调方法
  • 消息队列: 传统的多处理服务定义了先进先出(FIFO)队列抽象,用于管理传入和传出数据。尽管消息队列既简单又方便,但是它们不如其他一些通信技术高效
  • Cocoa 分布式对象: 分布式对象是一种 Cocoa 技术,可提供基于端口的通信的高级实现。尽管可以将这种技术用于线程间通信,但是强烈建议不要这样做,因为它会产生大量开销。分布式对象更适合与其他进程进行通信,尽管在这些进程之间进行事务的开销也很高

总结

本文主要对线程、进程、多线程的基本概念与原理,简单的了解

关于 GCD 的使用及底层原理会在后续进行详细分析