iOS底层探索 -- 多线程

1. 多线程概念问题

  • 线程的定义

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

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

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

    • 资源拥有:同一进程的线程共享本进程的资源(如:内存、CPU),进程之间是相互独立的

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

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

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

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

  • 多线程的意义

    • 能适当提高程序的执行效率
    • 能适当提高资源的利用率(CPU,内存)
    • 线程上的任务执行完成后,线程会自动销毁
    • 开启线程需要占用一定的内存空间(默认 每个线程都占 512K )
    • 如果开启大量的线程,会占用大量的内存
    • 程序设计更加复杂,比如线程之间的通讯,多线程共享数据
  • 线程和Runloop的关系

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

    • nonatomic非原子性,非线程安全,适合内存小的移动设备

    • atomic 原子属性(线程安全,需要消耗大量的资源),针对多线程设计的,默认值

      保证同一时间只有一个线程能够写入(可以同一时间多个线程取值)
      本身是一把自旋锁

    • 建议使用nonatomic声明属性,尽量避免多线程抢夺同一资源

    • 尽量将加锁,资源抢夺的业务逻辑交给服务器处理,减少客户端压力

  • COC 的桥接

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

2. 多线程原理

  • 多线程原理

    • CUP在单位时间片里快速在各个线程之前切换
    • 多核是为了加快执行的效率
  • 多线程生命周期

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e4PZRCdP-1588931804943)(https://user-gold-cdn.xitu.io/2020/5/8/171f23ec24188bc2?w=1167&h=394&f=png&s=70827)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FaQyPWJ4-1588931804964)(https://user-gold-cdn.xitu.io/2020/5/8/171f2472ba57bdeb?w=1137&h=518&f=png&s=82193)]

饱和策略:

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

3. 端口通讯

VC

    //1. 创建主线程的port
    // 子线程通过此端口发送消息给主线程
    self.myPort = [NSMachPort port];
    //2. 设置port的代理回调对象
    self.myPort.delegate = self;
    //3. 把port加入runloop,接收port消息
    [[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
    
    self.person = [[KCPerson alloc] init];
    [NSThread detachNewThreadSelector:@selector(personLaunchThreadWithPort:)
                             toTarget:self.person
                           withObject:self.myPort];
                           
    
- (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);

}

Person

- (void)personLaunchThreadWithPort:(NSPort *)port{
    
    NSLog(@"VC 响应了Person里面");
    @autoreleasepool {
        //1. 保存主线程传入的port
        self.vcPort = port;
        //2. 设置子线程名字
        [[NSThread currentThread] setName:@"KCPersonThread"];
        //3. 开启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];

    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"]);
    
    NSArray *messageArr = [message valueForKey:@"components"];
    NSString *dataStr   = [[NSString alloc] initWithData:messageArr.firstObject  encoding:NSUTF8StringEncoding];
    NSLog(@"传过来一些信息 :%@",dataStr);
}

解析:

1. 在 VC 中创建主线程的port(子线程通过此端口发消息给主线程),并设置 port  的代理回调对象
2. 将 port 加入到 runloop,接收 port 消息。
3. 实现 handlePortMessage 代理方法

4. GCD 初探

GCD 是一套纯 C 语言 API,提供了非常多的强大的函数,自动管理线程的生命周期(创建线程,调度任务,销毁线程)。只需要将任务添加到队列,并且指定执行任务的函数

任务是由block封装的(无参数,无返回值)

执行任务的函数有两种:

  • 异步 dispatch_async,不用等待当前语句执行完毕,就可以执行下一条语句,会开辟新的线程执行任务
  • 同步 dispatch_sync,必须等待当前语句执行完毕,才会执行下一条语句,不会开辟新的线程,在当前线程执行block任务

队列分为两种:

  • 串行队列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fdzsliej-1588931804967)(https://user-gold-cdn.xitu.io/2020/5/8/171f325d8797889b?w=627&h=222&f=png&s=32586)]

  • 并发队列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-96uuEc6I-1588931804979)(https://user-gold-cdn.xitu.io/2020/5/8/171f3260da4cf0a3?w=742&h=291&f=png&s=38456)]

那么还有下面两种队列:主队列和全局队列

  • 主队列dispatch_get_main_queue()

    • 专门用来在住线程上调度任务的队列,不会开启线程
    • 如果当前住线程正在有任务执行,那么无论主队列中当前添加了什么任务都不会被调度
  • 全局队列dispatch_get_global_queue()

    • 是一个并发队列
    • 在使用多线程开发是,如果队列没有特殊需求,在执行异步任务时,可以直接使用全局队列

队列和函数组合有以下四种情况:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q6RsjtHz-1588931804981)(https://user-gold-cdn.xitu.io/2020/5/8/171f32c21bc01ac7?w=1030&h=513&f=png&s=76597)]

接下来,看一下GCD的经典面试题

- (void)textDemo{
    
    dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
    // 1 5 2 4 3
}

上面代码打印结果是什么?

其实上面的代码很简单,打印结果是1 5 2 4 3

分析:
首先是打印 1 ,然后是并发队列加异步函数,这个操作是异步耗时,所以先执行打印 5,
然后是耗时操作,打印 2 ,接着又是并发队列加异步函数,同理先打印 4, 在打印3。

接着看下面的题目

dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);

    /***
     1 2 3
     0
     7 8 9
     */
    
    dispatch_async(queue, ^{
        // sleep(2);
        NSLog(@"1");
    });
    dispatch_async(queue, ^{
        NSLog(@"2");
    });
    // 堵塞 - 护犊子
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    // **********************
    NSLog(@"0");

    dispatch_async(queue, ^{
        NSLog(@"7");
    });
    dispatch_async(queue, ^{
        NSLog(@"8");
    });
    dispatch_async(queue, ^{
        NSLog(@"9");
    });
    // A: 1230789
    // B: 1237890
    // C: 3120798
    // D: 2137890

上面的结果是A C

分析:
首先是并发队列,然后是两个异步耗时操作,紧接着是一个同步函数,这样会堵塞后面的任务
的执行,再接着是两个异步操作。

所以,0 是在中间的,前面 1、2、3和后面7、8、9,顺序未知(依赖于任务复杂度和cpu的调度)

接着看下面的题目

下面的代码打印什么?

    dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");

上面的结果是1 5 2,然后死锁崩溃

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9fAnpBP5-1588931804983)(https://user-gold-cdn.xitu.io/2020/5/8/171f3abb629de0e6?w=603&h=316&f=png&s=28382)]

分析:
首先是串行队列,先打印1,然后是异步并发,耗时操作, 先打印 5,然后将打
印 2 的任务加到队列,然后将代码块加入队列,再将打印4 的任务加入队列,然后将同步
打印3 的任务加入队列,

而任务 4 的执行,必须在代码块执行完之后,而代码块是同步,必须等任务3执行完,
而任务 3,在等待任务 4 执行完,就造成了相互等待,死锁的问题

同理,将打印 4,放到同步函数前,一样会死锁

    dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    // 异步函数
    dispatch_async(queue, ^{
        NSLog(@"2");
        NSLog(@"4");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
    });
    NSLog(@"5");

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值