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

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

进程

定义

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

iOS开发中,一个App在内存中就是一个进程,且相互独立,只能访问本身的沙盒控件,这也是苹果运行可以流畅安全的一个主要缘由。面试

特色

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

线程

定义

线程的定义,主要能够归结为如下3点:后端

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

进程与线程的关系

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

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

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

  • 进程切换时,消耗的资源大,效率高。因此涉及到频繁的切换时,使用线程要好于进程。 一样若是要求同时进行而且又要共享某些变量的并发操做,只能用线程不能用进程并发

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

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

线程与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 查漏补缺 - 线程

相关文章
相关标签/搜索