阅读 647

iOS底层学习 - 多线程之基础原理篇

多线程是iOS开发中的一个重要的环节,不管是在日常的开发,还是面试中,都有着举足轻重的地位,所以打算开辟一个小专题,研究多线程相关的底层原理。这一篇章是第一篇,介绍一些基础的概念性的多线程。

进程

定义

当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存。

iOS开发中,一个App在内存中就是一个进程,且相互独立,只能访问自己的沙盒控件,这也是苹果运行能够流畅安全的一个主要原因。

特点

  • 独立性:是系统独立存在的实体,拥有自己独立的资源,有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户的进程不可以直接访问其他进程的地址空间。
  • 动态性:进程与程序的区别在于:程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集和,进程中加入了时间的概念。进程具有自己的生命周期和不同的状态,这些都是程序不具备的。
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响。

线程

定义

线程的定义,主要可以归结为以下3点:

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

进程与线程的关系

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。

  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间 的资源是独立的。

  • 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程 都死掉。所以多进程要比多线程健壮。

  • 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。 同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程

  • 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线 程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

  • 线程是处理器调度的基本单位,但是进程不是。

线程与Runloop的关系

  1. runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的, 是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里。
  2. runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。
  3. runloop在第一次获取时被创建,在线程结束时被销毁。
  4. 对于主线程来说,runloop在程序一启动就默认创建好了。
  5. 对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调

队列

定义

队列,又称为伫列(queue),是先进先出FIFO, First-In-First-Out)的线性表,在具体应用中通常用链表或者数组来实现。装载线程任务的队形结构。队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。

类型

队列的类型决定了任务的执行方式(并发、串行),队列包括以下几种:

  • 并发队列(Concurrent Dispatch Queue): 线程执行可以同时一起进行执行,不需要上一个执行完,就能执行下一个的。
  • 串行队列(Serial Dispatch Queue): 线程执行只能依次逐一先后有序的执行,等待上一个执行完,再执行下一个。
  • 主队列:绑定主线程,所有任务都在主线程中执行,有经过特殊处理的串行队列。
  • 全局队列:系统提供的并发队列。

同步、异步

同步 sync: 只能在当前线程按先后顺序依次执行任务,不具备开启新线程的能力。

异步 async: 在新的线程中执行任务,具备开启新线程的能力。

多线程

概念和原理

一个进程中可以并发多个线程同时执行各自的任务,叫做多线程。

分时操作系统会把CPU的时间划分为长短基本相同的时间区间,叫时间片,在一个时间片内,CPU只能处理一个线程中的一个任务,对于一个单核CPU来说,在不同的时间片来执行不同线程中的任务,就形成了多个任务在同时执行的“假象”。

多线程即为单位时间片里快速在各个线程之间切换

生命周期

  • 新建(New): 新建线程
  • 就绪(Runnable) : start之后就会runnable,然后等待cpu分配资源,进行调用
  • 运行(Running) : 当获得cpu调度之后就会到running状态
  • 阻塞(Block) : 当线程中任务异常是,比如sleep,或者死锁等操作之后就会造成线程阻塞.当阻塞解除之后不会直接到running状态而是又到就绪状态
  • 死亡(Dead) : 任务执行完成或者强制退出

优缺点

优点:

  1. 能适当提高程序的执行效率
  2. 能适当提高资源利用率(cpu,内存)
  3. 线程上的任务执行完成后,线程会自动销毁

缺点:

  1. 开启线程需要占用一定的内存空间(默认情况下,每个线程都占512KB)
  2. 如果开启大量的线程,会占用大量的内存空间,降低程序性能
  3. 线程越多,CPU在调用线程上的开销越大
  4. 程序设计更加复杂,比如线程间的通讯,多线程的数据共享

iOS中的多线程方案

  1. pthread:即POSIX Thread,缩写称为pthread,是线程的POSIX标准,是一套通用的多线程API,可以在Unix/Linux/Windows等平台跨平台使用。iOS中基本不使用。
  2. NSThread:苹果封装的面向对象的线程类,可以直接操作线程,比起GCDNSThread效率更高,由程序员自行创建,当线程中的任务执行完毕后,线程会自动退出,程序员也可手动管理线程的生命周期。使用频率较低。
  3. GCD:全称Grand Central Dispatch,由C语言实现,是苹果为多核的并行运算提出的解决方案,CGD会自动利用更多的CPU内核,自动管理线程的生命周期,程序员只需要告诉GCD需要执行的任务,无需编写任何管理线程的代码。GCD也是iOS使用频率最高的多线程技术。
  4. NSOperation:基于GCD封装的面向对象的多线程技术,常配合NSOperationQueue使用,使用频率较高。

GCD和NSOperation区别

  1. GCD仅仅支持FIFO队列,不支持异步操作之间的依赖关系设置。而NSOperation中的队列可以被重新设置优先级,从而实现不同操作的执行顺序调整。
  2. NSOperation支持KVO,可以观察任务的执行状态。
  3. GCD更接近底层,GCD在追求性能的底层操作来说,是速度最快的。
  4. 从异步操作之间的事务性,顺序行,依赖关系。GCD需要自己写更多的代码来实现,而NSOperation已经内建了这些支持
  5. 如果异步操作的过程需要更多的被交互和UI呈现出来,NSOperation更好。底层代码中,任务之间不太互相依赖,而需要更高的并发能力,GCD则更有优势

小结图表

线程池

定义

线程池是多线程处理的一种形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池中的线程都是后台线程。每个线程都有默认的堆栈大小,以默认的优先级运行,并处在多线程单元中。

执行流程

  1. 判断线程池大小是否小核心线程池大小
  2. 如果小于.创建线程执行任务
  3. 如果不小于 判断工作队列是否已满,不满,将任务提交到工作队列,创建线程执行任务,如果已满,判断线程是否都在工作,如果都在工作交给饱和策略,如果没满创建线程执行任务

饱和策略

  1. AbortPolicy:默认策略。直接抛出RejectedExecutionExeception异常阻止系统正常运行,该异常由调用者捕获
  2. CallerRunsPolicy:调节机制。既不抛弃也不报异常。将任务回退给调用者
  3. DisOldestPolicy:丢掉等待最久的任务
  4. DisCardPolicy:直接丢弃任务

线程间通讯

苹果的官方文档中,给出了几种线程间通信的方式:

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

端口通信例子

对于performSelector的通信,平时在开发中运用的比较多,就不详细讲述了,我们讲一个线程之前,运用端口来传递消息的例子来加深印象。

首先我们创建一个类来发送消息:

@interface WYPerson : NSObject
- (void)personLaunchThreadWithPort:(NSPort *)port;
@end
复制代码
#import "WYPerson.h"

@interface WYPerson()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *vcPort;
@property (nonatomic, strong) NSPort *myPort;
@end

@implementation WYPerson


- (void)personLaunchThreadWithPort:(NSPort *)port{
    
    NSLog(@"VC 响应了Person里面");
    @autoreleasepool {
        ✅//1. 保存主线程传入的port
        self.vcPort = port;
        ✅//2. 设置子线程名字
        [[NSThread currentThread] setName:@"WYPersonThread"];
        ❗️//3. 开启runloop(重点在这)
        [[NSRunLoop currentRunLoop] run];
        ✅//4. 创建自己port
        self.myPort = [NSMachPort port];
        ✅//5. 设置port的代理回调对象(NSMachPortDelegate)
        self.myPort.delegate = self;
        ✅//6. 完成向主线程port发送消息
        [self sendPortMessage];
    }
}


/**
 *   完成向主线程发送port消息
 */

- (void)sendPortMessage {
 
    NSData *data1 = [@"WY" dataUsingEncoding:NSUTF8StringEncoding];

    NSMutableArray *array  =[[NSMutableArray alloc]initWithArray:@[data1,self.myPort]];
    ✅// 发送消息到VC的主线程// 第一个参数:发送时间。// msgid 消息标识。// components,发送消息附带参数。// reserved:为头部预留的字节数
    [self.vcPort sendBeforeDate:[NSDate date]
                          msgid:10086
                     components:array
                           from:self.myPort
                       reserved:0];
    
}

#pragma mark - NSMachPortDelegate 处理端口传递信息

- (void)handlePortMessage:(NSPortMessage *)message{
    NSLog(@"person:handlePortMessage  == %@",[NSThread currentThread]);
    NSLog(@"从VC 传过来一些信息:");
    NSLog(@"components == %@",[message valueForKey:@"components"]);
    NSLog(@"receivePort == %@",[message valueForKey:@"receivePort"]);
    NSLog(@"sendPort == %@",[message valueForKey:@"sendPort"]);
    NSLog(@"msgid == %@",[message valueForKey:@"msgid"]);
}


复制代码

接着我们创建PortViewController用来接收消息和回调消息给发送者

#import "PortViewController.h"
#import <objc/runtime.h>
#import "WYPerson.h"

@interface PortViewController ()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *myPort;
@property (nonatomic, strong) WYPerson *person;

@end

@implementation PortViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"Port线程通讯";
    self.view.backgroundColor = [UIColor whiteColor];

    ✅//1. 创建主线程的port// 子线程通过此端口发送消息给主线程
    self.myPort = [NSMachPort port];
    ✅//2. 设置port的代理回调对象
    self.myPort.delegate = self;
    ❗️//3. 把port加入runloop,接收port消息(此时主线程已经再跑,不需要run)
    [[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
    
    self.person = [[WYPerson alloc] init];
    [NSThread detachNewThreadSelector:@selector(personLaunchThreadWithPort:)
                             toTarget:self.person
                           withObject:self.myPort];
    
}

#pragma mark - NSMachPortDelegate

- (void)handlePortMessage:(NSPortMessage *)message{
    
    NSLog(@"VC == %@",[NSThread currentThread]);
    
    NSLog(@"从person 传过来一些信息:");
    //会报错,没有这个隐藏属性
    //NSLog(@"from == %@",[message valueForKey:@"from"]);
    
    NSArray *messageArr = [message valueForKey:@"components"];
    NSString *dataStr   = [[NSString alloc] initWithData:messageArr.firstObject  encoding:NSUTF8StringEncoding];
    NSLog(@"传过来一些信息 :%@",dataStr);
    NSPort  *destinPort = [message valueForKey:@"remotePort"];
    
    if(!destinPort || ![destinPort isKindOfClass:[NSPort class]]){
        NSLog(@"传过来的数据有误");
        return;
    }
    
    NSData *data = [@"VC收到!!!" dataUsingEncoding:NSUTF8StringEncoding];
    
    NSMutableArray *array  =[[NSMutableArray alloc]initWithArray:@[data,self.myPort]];
    
    ❗️❗️❗️// 非常重要,如果你想在Person的port接受信息,必须加入到当前主线程的runloop
    [[NSRunLoop currentRunLoop] addPort:destinPort forMode:NSDefaultRunLoopMode];
    
    NSLog(@"VC == %@",[NSThread currentThread]);
    
    BOOL success = [destinPort sendBeforeDate:[NSDate date]
                                        msgid:10010
                                   components:array
                                         from:self.myPort
                                     reserved:0];
    NSLog(@"%d",success);

}


- (void)getAllProperties:(id)somebody{
    
    u_int count = 0;
    objc_property_t *properties = class_copyPropertyList([somebody class], &count);
    for (int i = 0; i < count; i++) {
        const char *propertyName = property_getName(properties[i]);
         NSLog(@"%@",[NSString stringWithUTF8String:propertyName]);
    }
}

复制代码

打印结果如下:

通过上面的例子,我们已经实现了线程之间的通信

稍微总结一下NSPort的使用要点:

  1. NSPort对象必须添加到要接收消息的线程的Runloop中,必须由Runloop来进行管理
  2. 接收消息的对象实现NSPortDelegate协议的-handlePortMessage:方法来获取消息内容

参考资料

iOS底层原理探索—多线程的本质

iOS 查漏补缺 - 线程