ios多线程(一)

266 阅读7分钟

进程和线程

1.1 进程

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

1.2 线程

线程是进程的基本执行单元,进程中的所有任务都在线程中执行,因此,一个进程中至少要有一个线程。iOS程序启动后会默认开启一个主线程,也叫UI线程。

1.3 进程与线程的关系

  • 地址空间:同一进程中的地址空间可以被本进程中的多个线程共享,但进程与进程之间的地址空间是独立的
  • 资源拥有:同一进程中的资源可以被本进程中的所有线程共享,如内存I/OCUP等等,但进程与进程之间的资源是相互独立的
  • 一个进程中的任一线程崩溃后,都会导致整个进程崩溃,但进程奔溃后不会影响另一个进程
  • 进程可以看做是线程的容器,每个进程都有一个程序运行的入口,但线程不能独立运行,必须依存于进程

1.4 线程与Runloop的关系

  • 线程Runloop是一一对应的,一个Runloop对应一个核心线程,为什么说是核心,因为Runloop是可以嵌套的,但核心的只有一个,他们的对应关系保存在一个全局字典里
  • Runloop是来管理线程的,线程执行完任务时会进入休眠状态,有任务进来时会被唤醒开始执行任务(事件驱动)
  • Runloop在第一次获取时被创建,线程结束时被销毁
  • 主线程的Runloop在程序启动时就默认创建好了
  • 子线程的Runloop是懒加载的,只有在使用时才被创建,因此在子线程中使用NSTimer时要注意确保子线程的Runloop已创建,否则NSTimer不会生效。

1.5 线程的生命周期

线程的生命周期可分为五个阶段:新建就绪调用阻塞死亡

09EF1E7F-5EF4-4678-93BC-04EA17EFF92A.png

多线程

2.1概念和原理

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

上图中,CPU在时间片t3开始执行线程3中的任务,但任务还没执行完,来到了t4,开始执行线程4中的任务,在t4这个时间片内就执行完了线程4的任务,到t5时接着执行线程3的任务。 现在都是多核CPU,每个核心都可以单独处理任务,实现“真正”的多线程,但是一个App动辄几十个并发线程,那么每个核心仍然以上述原理实现多线程。

2.2 ios中常用的多线程

在iOS中,有下列几种多线程的使用方式:

1.pthread:即POSIX Thread,缩写称为Pthread,是线程的POSIX标准,是一套通用的多线程API,可以在Unix/Linux/Window等平台跨平台使用。iOS中基本不使用。

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

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

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

线程池

  • 线程池(Thread Pool)顾名思义就是一个管理多个线程生命周期的池子。iOS开发中不会直接接触到线程池,这是因为GCD已经包含了线程池的管理,我们只需要通过GCD获取线程来执行任务即可。
  • 线程的生命周期 一个线程的生命周期包括创建--就绪--运行--死亡这四个阶段,我们可以通过阻塞、退出等来控制线程的生命周期。

线程间的通讯

4.1 几种线程间的通讯方式

上图的表格是按照技术复杂度由低到高顺序排列的,其中后两种只能在OS X中使用。

  • Direct messaging:这是大家非常熟悉的-performSelector:系列。
  • Global variables...:直接通过全局变量、共享内存等方式,但这种方式会造成资源抢夺,涉及到线程安全问题。
  • Conditions:一种特殊的锁--条件锁,当使用条件锁使一个线程等待(wait)时,该线程会被阻塞并进入休眠状态,在另一个线程中对同一个条件锁发送信号(single),则等待中的线程会被唤醒继续执行任务。
  • Run loop sources:通过自定义Run loop sources来实现,后面的文章会单独研究Run loop
  • Ports and sockets:通过端口和套接字来实现线程间通讯。

4.2 线程间通讯的示例

  • 首先在主线程中创建port,设置代理,然后把port添加到runloop中,开启子线程。
- (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消息
    [[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
    
    self.person = [[KCPerson alloc] init];
    [NSThread detachNewThreadSelector:@selector(personLaunchThreadWithPort:)
                             toTarget:self.person
                           withObject:self.myPort];
    
}
  • 在这里,子线程通Port发送消息给主线程。
- (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];
    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];
    
}
  • 主线程拿到数据后,在发送消息给子线程。
- (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
    [[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)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"]);
}

至此,线程间通过port实现通讯的例子就完成了。