17、iOS底层分析 - 多线程(一)相关概念

一、进程与线程

1.1进程
      进程是操作系统分配资源和调度的基本单位。在iOS上,一个APP运行起来的实例就是一个进程,每个进程在内存中都有自己独立的地址段(专用且受保护的内存空间),拥有独立运行所需的全部资源。

目前iOS都是单进程的,也就是说进程可以理解为目前系统中正在运行的应用程序。


1.2 线程
      线程是进程的基本执行单元,进程中的所有任务都在线程中执行,因此,一个进程中至少要有一个线程。iOS 程序启动后会默认开启一个主线程,也就 UI 线程。UI必须在主线程中进行操作,否者可能会出现一些未知问题。


1.3 进程与线程的关系
    一个进程可以有多个线程,但是至少要有一条线程。
    地址空间:同一个进程中的地址空间可以被本进程中的多个线程共享,但是进程与进程之间的地址空间是独立的。
    资源拥有:同一进程中的资源可以被本进程中的所有线程共享,如内存、I/O、CPU等等,但是进程与进程之间的资源是相互独立的。
    一个进程中的任一线程崩溃后,都会导致整个进程崩溃,但进程崩溃后不会影响另一个进程

执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序的中,由应用程序提供多个线程执行控制
    线程是处理器调度的基本单元,进程不是。
    进程可以看做是线程的容器,每个进程都有一个程序运行的入口,但是线程不能独立运行,必须依存于进程
 
1.4线程与RunLoop的关系
    线程与RunLoop是一一对应的,一个RunLoop对应一个核心线程。为什么说是核心,因为RunLoop是可以嵌套的,但核心的只有一个,他们的对应关系保存一个全局字典里
    RunLoop是来管理线程的,线程执行完任务时会进入休眠状态,有任务来时会被唤醒开始执行任务(事件驱动)
    RunLoop在第一次获取时被创建,线程结束时被销毁
    主线程的RunLoop在程序启动时就默认创建好了
    子线程的RunLoop是懒加载的,只有在使用时才被创建,因此在子线程中使用NSTimer时要注意确保子线程的RunLoop 已经创建,否者NSTimer 不会生效。


 二、多线程

2.1概念及原理
     一个进程中可以并发多个线程同时执行各自的任务,叫多线程。分时操作系统会把CPU 的时间划分为长短基本相同的时间区间,叫“时间片”,在一个时间片内,CPU只能处理一个线程中的任务,对于一个单核CPU 来说,在不同的时间片来执行不同线程中的任务,就形成了多个任务在同时执行的“假象”:
    (同一时间,CPU 只能处理1条线程,只有1 条线程在执行。多线程并发执行,其实是CPU 快速地在多条线程之间调度(切换)。如果CPU 调度线程的的时间足够快,就造成了多线程并发执行的"假象")

    上图中,CPU在时间片 t3开始执行线程3中的任务,但任务还没有执行完,来到了 t4,开始执行线程4中的任务,在t4 这个时间片内就执行完了线程4 的任务,到t5 时接着执行线程3 的任务。
    现在很多都是多核CPU,每个核心都可以单独处理任务,实现”真正“的多线程,但是一个APP动辄几十个并发线程,那么每个核心任然以上述原理实现多线程。
    多线程的优缺点:
    优点:
    能适当提高程序的执行效率
    能适当提高资源利用率(CPU、内存利用率)
    缺点:
    开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用 512KB),如果开启大量的线程,会占用大量的内存空间,降低程度的性能。
    线程越多,CPU 在调度线程上的开销就越大
 
2.2、任务、队列
    1、GCD中执行任务有两种方式 同步执行(sync)异步执行(async)
    同步(sync):同步添加任务到对列表中, 在添加的任务执行结束之前,会一直等待,知道队列里面的任务完成之后再继续执行。会阻塞线程,只能在当前线程中执行任务(当前线程不一定是主线),不具备开启新线程的能力。
    异步(async):线程会立即返回,无需等待就会继续执行下面的任务,不阻塞当前线程,可以在新的线程中执行任务,具备开启新线程的能力(并不一定开始新线程)。如果不是添加到主队列上,异步会在子线程中执行任务。
 
    队列
    存放任务等待执行的队列。队列是一种特殊的线性表,才用FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列释放一个任务
    在GCD 中有两种队列:串行队列和并发队列。
 
 2.3、iOS中的几种多线程
    在iOS中,有下列几种多线程的使用方式。
    1、Pthread

POSIX thread,缩写Pthread,是线程的POSIX 标准,是一套通用的多线程API,可以在Unix/Linux/Windows等平台跨平台使用。iOS中基本不使用。
    2、NSTread

苹果封装的面向对象的线程类,可以直接操作线程,比起GCD,NSThread效率更高,由程序员自行创建,当线程中的任务执行完毕后,线程会自动退出,程序员也可手动管理线程的生命周期。使用频率较低。

NSTread 后面会单独再分析,分析完会附上连接地址。
    3、GCD

全程Grand Central Dispatch,由C语言实现,是苹果味多核的并行运算踢出的解决方案,GCD 会自动利用更多的CPU 内核,自动管理线程的生命周期。程序员只需要告诉GCD 需要执行的任务,无需写f任何管理线程的代码。GCD 也是iOS使用频率最高的多线程技术。

GCD 后面会单独再分析,分析完会附上连接地址。
    4、NSOperation

基于GCD 封装的面向对象的多线程技术,常配合NSOperationQueue 使用,使用效率较高。

NSOperation 后面会单独再分析,分析完会附上连接地址。

    //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];
    }];


    //NSThread 的几种初始化方法
    NSThread * thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(threadDemo1:) object:@"thread1"];
    NSThread * thread2 = [[NSThread alloc] init];
    NSThread * thread3 = [[NSThread alloc] initWithBlock:^{
        NSLog(@"NSThread initWithBlock");
    }];


三、线程池

    线程池(Thread Pool)
    顾名思义就是一个u管理多线程生命周期的池子。iOS开发中不会直接接触到线程池,这是因为GCD 已经包含了线程池的管理,我们只需要通过GCD 获取线程来执行任务即可。


    线程的生命周期
    一个线程的生命周期包括: 创建 - 就绪 - 运行 - 死亡 这四个阶段。我们可以通过阻塞、退出等来控制线程的生命周期。

线程调度的时候,首先会来到线程池,看当前是否有可用的的线程。进入线程池
判断线程池大小是否小于核心线程池大小(也就是判断线程池是否已经满了),这个地方需要说一下。线程池有最大值,有一个核心线程池大小。
例如线程池最大值是100,核心大小值80,当前线程池大小60.60<80 这个时候就会去创建线程去执行

饱和策略
 • AbortPolicy           直接抛出 RejectedExecutionExeception 异常来阻止系统正常运行
 • CallerRunsPolicy  将任务回退到调用者
 • DisOldestPolicy    丢掉等待最久的任务‘
 • DisCardPolicy       直接丢弃任务
 这四种拒绝策略均实现的 RejectedExecutionHandler 接口。

四、线程安全

在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
 多线程操作共享数据不会出现想不到的结果就是线程安全的,否则,是线程不安全的。
 但是多个线程访问同一个对象时,如何保证证线程是安全的?这个时候就需要想办法解决,那么就用到了锁。
 举例:

 模拟购票系统,两个线程去访问数据,中间有可能两个线程同时访问拿到同一个值进行了操作,这样就造成了数据不准确。所以需要加锁,以保证每条线程访问的时候拿到的都是正确的数据。

@property (nonatomic, assign) NSInteger tickets;
@property (nonatomic, strong) NSMutableArray *mArray;

//线程安全
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.tickets = 20;
    // 资源问题
    // 数据库
    // 增删改查
    // 199 -- 100
//     1. 开启一条售票线程
        NSThread *t1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
        t1.name = @"售票 A";
        [t1 start];
    
        // 2. 再开启一条售票线程
        NSThread *t2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
        t2.name = @"售票 B";
        [t2 start];
}
- (void)saleTickets {
    
    // runloop & 线程 不是一一对应
    while (YES) {
        // 0. 模拟延时
        //NSObject *obj = [[NSObject alloc] init];
        //obj 是自己的临时对象,对其他访问该区域的无影响
        //可以锁self 那么访问该方法的时候所有的都锁住,可以根据需求特定锁
        @synchronized(self){
            // 递归 非递归
            [NSThread sleepForTimeInterval:1];
            // 1. 判断是否还有票
            if (self.tickets > 0) {
                // 2. 如果有票,卖一张,提示用户
                self.tickets--;
                NSLog(@"剩余票数 %zd %@", self.tickets, [NSThread currentThread]);
            } else {
                // 3. 如果没票,退出循环
                NSLog(@"没票了,来晚了 %@", [NSThread currentThread]);
                break;
            }
            //在锁里面操作其他的变量的影响
            [self.mArray addObject:[NSDate date]];
            NSLog(@"%@ *** %@",[NSThread currentThread],self.mArray);
        }
    }
}

@synchronized(){}

互斥锁
    保证锁内的代码,同一时间,只有一条线程能够执行。
    互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差。
 互斥锁参数
    能够加锁的任意 NSObject 对象
    注意:锁对象一定要保证d所有的线程都能够访问
    如果代码中只有一个地方需要加锁,大多都是用 self,这样可以避免单独再创建一个锁对象

属性的线程安全

@property (nonatomic, copy) NSString *name;

 atomic     是原子属性,是为多线程开发准备的,是默认属性!
 atomic本身仅仅在属性的 `setter` 方法中,增加了锁(自旋锁),能够保证同一时间,只有一条线程对属性进行`写`操作
 同一时间 单(线程)写多(线程)读的线程处理技术
 nonatomic  是非原子属性
 没有锁!性能高!
 
 atomic 线程安全,需要消耗大量的资源
 nonatomic:非线程安全,适合内存小的移动设备
 iOS开发建议
 属性尽量都声明为 nonatomic
 尽量避免多线程抢到同一块资源
 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力。

// 在 OC 中,如果同时重写 了 setter & getter 方法,系统不再提供 _成员变量,需要使用合成指令
// @synthesize name 取个别名:_name
@synthesize name = _name;
#pragma mark - 模拟原子属性示例代码
- (NSString *)name {
    return _name;
}
- (void)setName:(NSString *)name {
    //增加一把锁,就能够保证一条线程在同一时间写入!
    @synchronized (self) {
        _name = name;
    }
}

关于锁的更多分析,后面再进行分析。

五、线程间的通讯

1、几种线程间的通讯方式
    在面试中,经常备面试问到线程间是如何通讯的,很多人会回答在子线程获取数据,切换回主线程刷新UI。那么还有没有其他的方式呢? 苹果 官方文档 给我们列出线程间通讯的几种方式
 
上图的表格是按照技术复杂度由低到高顺序排列的,其中后两种只能在 OS X 中使用。
    1、Direct message:这是大家非常熟悉的 -performSelector:系列
    2、Global variables...: 直接通过全局变量、共享内存等方式,但这种方式会造成资源抢夺,涉及到线程安全问题。
    3、Conditions:一种特殊的锁 -- 条件锁,当使用条件锁是一个线程等待(wait)时,该线程会被阻塞并进入休眠状态,在另一个线程中对统一个条件锁发送信号(single),则等待中的线程会被唤醒继续执行任务。
    4、Run loop sources:通过自定义Run loop sources 来实现,后面的文字会单独研究 RunLoop。
    5、Ports and sockets:通过端口和套接字来实现线程间通讯。
 
 2、线程间通讯示例

-performSelector:系列

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//        [self performSelector:@selector(ceshi1)];
//        2、使用了NSTimer 实现2秒后调用,NSTimer需要启用RunLoop
//        [self performSelector:@selector(ceshi2:) withObject:@"测试" afterDelay:2];
//        [[NSRunLoop currentRunLoop] run];
//        3、
//        [self performSelector:@selector(ceshi3:ob:) withObject:@"测试" withObject:@"多线程"];
        /**
            回到主线程
            waitUntilDone:
            YES:必须执行完主线程才能往下走   先执行 ceshi2: 再执行 ”走不走“
            NO:不需要执行完主线程,可以先往下走.   先执行 ”走不走“ 再执行ceshi2:
         其实当waitUntilDone:YES ,是用到了 RunLoop的知识,让其一直在等待 直到完成,才往下走。
        */
        [self performSelectorOnMainThread:@selector(ceshi2:) withObject:@"测试123" waitUntilDone:YES];
        NSLog(@"走不走?");
    });
    
//    定义一个全局变量,通过加锁的方式在不同线程中访问,来实现线程通讯的目的
}
-(void)ceshi1
{
    NSLog(@"12345");
}
-(void)ceshi2:(id)objc
{
    NSLog(@"12345  %@",objc);
    dispatch_async(dispatch_get_main_queue(), ^{
        self.view.backgroundColor = [UIColor orangeColor];
    });
}
-(void)ceshi3:(id)objc ob:(id)ob
{
    NSLog(@"12345  %@  %@",objc ,ob);
}

NSPort 线程通讯

其实是通过端口号进行连接通讯,更快。代码中都做了注释,就不过多介绍直接上代码

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

@interface PortViewController ()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *myPort;
@property (nonatomic, strong) KCPerson *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消息。在主线程中,这个地方不能再RunLoop run
//   * self.myPort 是当前收发消息的时候,需要保持其生命周期,所以要加入运行循环。
    [[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
    
    self.person = [[KCPerson 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(@"localPort == %@",[message valueForKey:@"localPort"]);
//    NSLog(@"remotePort == %@",[message valueForKey:@"remotePort"]);
//    NSLog(@"receivePort == %@",[message valueForKey:@"receivePort"]);
//    NSLog(@"sendPort == %@",[message valueForKey:@"sendPort"]);
//    NSLog(@"msgid == %@",[message valueForKey:@"msgid"]);
//    NSLog(@"components == %@",[message valueForKey:@"components"]);
    //会报错,没有这个隐藏属性
    //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。 destinPort 是临时变量所以需要加入运行循环
    [[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]);
    }
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
@end
//KCPerson.h
#import <Foundation/Foundation.h>

@interface KCPerson : NSObject
- (void)personLaunchThreadWithPort:(NSPort *)port;
@end


//  KCPerson.m
#import "KCPerson.h"

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

@implementation KCPerson

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


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

- (void)sendPortMessage {
 
    NSData *data1 = [@"Gavin" dataUsingEncoding:NSUTF8StringEncoding];
    NSData *data2 = [@"Cooci" 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"]);
}


@end

输出的结果。过程就是VC 和 person 之间通过端口进行识别连接进行通讯,子线程调用了person,然后person又通过端口给 VC 发送消息,VC相应回调然后修改数据后又传给了person。

2020-03-12 22:53:18.165625+0800 004---线程通讯[47226:829715] VC 响应了Person里面
2020-03-12 22:53:18.216873+0800 004---线程通讯[47226:827356] VC == <NSThread: 0x600001ea23c0>{number = 1, name = main}
2020-03-12 22:53:18.217275+0800 004---线程通讯[47226:827356] 从person 传过来一些信息:
2020-03-12 22:53:18.217534+0800 004---线程通讯[47226:827356] 传过来一些信息 :Gavin
2020-03-12 22:53:18.220513+0800 004---线程通讯[47226:827356] VC == <NSThread: 0x600001ea23c0>{number = 1, name = main}
2020-03-12 22:53:18.220733+0800 004---线程通讯[47226:827356] 1
2020-03-12 22:53:18.221277+0800 004---线程通讯[47226:827356] person:handlePortMessage  == <NSThread: 0x600001ea23c0>{number = 1, name = main}
2020-03-12 22:53:18.221480+0800 004---线程通讯[47226:827356] 从VC 传过来一些信息:
2020-03-12 22:53:18.221783+0800 004---线程通讯[47226:827356] components == (
    <5643e694 b6e588b0 212121>,
    "<NSMachPort: 0x600003ca08f0>"
)
2020-03-12 22:53:18.221993+0800 004---线程通讯[47226:827356] receivePort == <NSMachPort: 0x600003cac580>
2020-03-12 22:53:18.222145+0800 004---线程通讯[47226:827356] sendPort == <NSMachPort: 0x600003ca08f0>
2020-03-12 22:53:18.222303+0800 004---线程通讯[47226:827356] msgid == 10010
发布了83 篇原创文章 · 获赞 12 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/shengdaVolleyball/article/details/104808306