iOS底层-24:多线程

线程和进程

进程

  1. 进程是指系统中正在运行的一个应用程序
  2. 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内。
  3. 通过活动监视器可以查看Mac系统中所开启的进程

线程

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

线程和进程的关系

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

  1. 一个进程奔溃后,在保护模式下不会对其他进程产生影响。但是一个线程崩溃整个进程都会死掉

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

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

  4. 线程是处理器调度的基本单元,但进程不是。

  5. 线程没有地址空间,线程包含在进程地址空间中。

线程和runloop的关系

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

多线程的意义

优点:

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

缺点:

  • 开启线程需要占用一定的内存空间(默认情况下,一个线程512KB
  • 如果开启大量的线程,会占用大量的内存,降低程序的性能。
  • 线程越多,CPU在调用线程上的开销就越大
  • 程序设计更加复杂,比如线程间的通信、多线程间的数据共享。
时间片

时间片的概念:CPU在多个任务之间进行快速的切换,这个时间间隔就是时间片

  • 单核CPU:同一时间,CPU只能处理1个线程
    • 换言之,同一时间只有1个线程在执行
  • 多线程同时执行:
    • 实际上是CPU快速的在多个线程之间切换
    • CPU调度线程的时间足够快,就造成了多线程“同时执行”的效果
  • 如果线程非常多
    • CPU会在N个线程中切换,消耗大量的CPU资源
    • 每个线程被调度的次数会降低,线程的执行效率低。
多线程技术方案

iOS中多线程方案,主要有pthread、NSThread、GCD、NSOperation

简单例子

下面演示这4种方式开启线程的简单实例。

    //0: pthread
    
    /**
     pthread_create 创建线程
     参数:
     1. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾
     同时不需要 `*`
     2. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的)
     3. 线程要执行的`函数地址`
     void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
     (*): 函数名
     (void *): 参数类型,void *
     4. 传递给第三个参数(函数)的`参数`
     
     返回值:C 语言框架中非常常见
     int
     0          创建线程成功!成功只有一种可能
     非 0       创建线程失败的错误码,失败有多种可能!
     */
    
    // 1: pthread
    pthread_t threadId = NULL;
    //c字符串
    char *cString = "HelloCode";
    //    NSString *ocString = @"Gavin";
    //延伸到: OC--C的混编 尤其在智能家居,SDK封装
    //抛出一个问题: 在ARC需要这样操作,在MRC不需要
    // OC prethread -- 跨平台
    // 锁
    int result = pthread_create(&threadId, NULL, pthreadTest, cString);
    if (result == 0) {
        NSLog(@"成功");
    } else {
        NSLog(@"失败");
    }
    // 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];
    }];
C与OC的桥接
  • __bridge只做类型转换,不修改对象(内存)管理权
  • __bridge_retained(也可以使用CFBridgingRetain),将Objective-C对象转换成Core Foundation的对象,同时将内存管理权也交给了Core Foundation,后续需要使用CFRelease或相关方法来释放对象
  • __bridge_transfer(也可以使用CFBridgingRelease),将Core Foundation对象转换成Objective-C的对象,同时将内存管理权交给ARC
线程的生命周期
  • 创建一个新线程,调用start方法时,并没有立即执行,只是进入了就绪状态。而是经过短暂的等待,等到CPU调度才真正执行
  • 直接调用exitcancle等可以直接终止线程,执行完毕也会终止。
    cancle不能取消正在执行的任务。
线程池

线程池是为了管理线程而提出的一个概念。



核心线程在没有任务的时候也不会被回收,而非核心线程是有存活时间的。
那么线程池的执行流程是怎么样的呢?


饱和策略

  • AbortPolicy:直接抛出RejectedExecutionExeception异常,阻止系统正常运行
  • CallerRunsPolicy:将任务回退给主线程
  • DisCardOldSetPolicy:丢弃等待最久的任务,替换为当前进来的任务执行
  • DisCardPolicy:丢弃进来的任务,不执行也不抛出异常

任务执行速度分析

线程优先级threadPriority越高是否任务执行速度越快?

threadPriority是一个double类型,取值(0~1),在iOS8.0之后被qualityOfService替代

typedef NS_ENUM(NSInteger, NSQualityOfService) {
    NSQualityOfServiceUserInteractive = 0x21,
    NSQualityOfServiceUserInitiated = 0x19,
    NSQualityOfServiceUtility = 0x11,
    NSQualityOfServiceBackground = 0x09,
    NSQualityOfServiceDefault = -1
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));

NSQualityOfServiceUserInteractive用户交互优先级最高。

答案:任务的执行速度,除了看优先级,还要看任务的难易程度,消耗资源的大小,以及cpu的调度。就算优先级高,一个难得任务也会比简单任务执行的慢。

资源抢夺

多线程同时访问一块资源时,可能会引发数据错乱和数据安全问题。一般需要加锁来保证每个线程看到的数据是一致的,同时保证每个线程对数据的修改一致。
使用较多的是下面两种锁:

  • 自旋锁
  • 互斥锁
自旋锁

一个执行单元想要访问被自旋锁保护的共享资源必须先得到锁,在访问完共享资源后必须释放锁。如果在获取自旋锁时,没有任何执行单元持有该锁,那么将立即得到锁;如果在获取时,锁已经有了持有者,那么获取锁的操作将自旋在那里,直到自选锁的持有者释放了锁。

线程会反复检查锁变量是否可用,由于线程在这一过程中一直保持执行,因此是一种忙等(忙碌等待)。一旦获取了自旋锁,线程会一直持有该锁,直至显式释放自旋锁。

自选锁可能会存在两个问题:

  • 死锁:
    试图递归的获得自旋锁必然会造成死锁:递归程序的持有实例在第二实例循环,试图获得相同的自旋锁,不会释放此自旋锁。

    • 递归程序中使用自旋锁应遵循以下策略:
      递归程序不能在持有自旋锁时调用它自己,也不能在递归时试图获得相同的自旋锁。
  • 过多的占用系统资源:
    如果不加限制,由于申请者一直在循环等待,不成功也不会睡眠,会持续的尝试。单cpu是自旋锁会让其他进程动不了。

由此可见自旋锁比较适用于锁的保持者保持时间比较短的情况。正是由于锁的持有者保存锁的时间非常短,所以才旋转自旋而不是睡眠。自旋锁的效率是远高于互斥锁。

互斥锁

互斥锁和自旋锁一样,都是用于线程同步的锁。保护一个对象,使其同时只能被一个线程保护。同一线程多次加锁会造成死锁。

  • 与自旋锁不同的是,当线程访问的变量正在被其线程访问时,互斥锁不会反复询问,而是进入休眠(挂起状态 不占用cpu资源),直到前一个线程解除对这个变量的锁定,第二个线程被`唤醒并继续执行,同时锁定这个变量。

互斥锁还需要注意一下几点:

  1. 互斥锁的锁定范围应该尽量小,范围越大,效率越差。
  2. 能够加锁任意NSObject对象
    3.锁对象一定要保证所有的线程都能访问
    4.如果代码中只有一个地方需要加锁,大多使用self,这样可以避免单独再创建一个锁对象

atomic和nonatomic

atomicnonatomic都是用来修饰属性的,以下是他们之间的区别:

atomic

  • 原子属性,是为多线程开发准备的,是默认属性
  • 仅在setter方法中增加了自旋锁,保证了同一时间只有一条线程可以对属性进行写操作。多读单写
  • 多用于Mac开发
  • 线程安全,需要消耗大量的资源,而且不是绝对意义上的线程安全

nonatomic

  • 非原子属性的,没有锁效率高
  • 移动开发中常用。

iOS开发的建议:所有属性都声明nonatomic,尽量避免多线程抢夺同一资源,尽量将加锁、抢夺资源的业务逻辑交给服务端处理,减少客户端的压力

线程通讯

NSThread可以使用performSelectorInBackground
performSelector: onThread:performSelectorOnMainThread在主线程和子线程中切换。
GCD可以使用dispatch_async开启子线程,也可以使用dispatch_get_main_queue()回到主线程。

端口通讯

线程之间的通讯还可以使用NSPort

了解NSPort
-NSPort是描述通信通道的抽象类。在两个NSPort对象之间通过- (BOOL)sendBeforeDate:(NSDate *)limitDate msgid:(NSUInteger)msgID components:(nullable NSMutableArray *)components from:(nullable NSPort *)receivePort reserved:(NSUInteger)headerSpaceReserved;发送消息和- (void)handlePortMessage:(NSPortMessage *)message;接收消息,来实现线程(或进程或应用)之间的通信。

  • NSPort对象必须作为输出源添加到NSRunLoop对象中,来实现线程不退出。
  • NSPort有3个子类NSMachPortNSMessagePortNSSocketPort

线程间通信的思路

  1. 在主线程创建一个NSPort的实例portA,并添加到主线程runloop
  2. 创建一个子线程S,将portA作为参数发送到线程S
  3. 在线程S中再创建一个本地的NSPort的实例portB,也添加到runloop
  4. portBportA发送消息,将线程Srunloop执行
  5. 主线程收到线程S的消息
  6. 主线程向线程S发送消息
  7. 通过handlePortMessage:代理方法接收消息。

实例代码

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

- (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);

}


@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
        [[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"]);
}

打印结果如下:

 VC == <NSThread: 0x600000684900>{number = 1, name = main}
 从person 传过来一些信息:
 传过来一些信息 :Gavin
 VC == <NSThread: 0x600000684900>{number = 1, name = main}
1
 person:handlePortMessage  == <NSThread: 0x600000684900>{number = 1, name = main}
 从VC 传过来一些信息:
 components == (
    {length = 11, bytes = 0x5643e694b6e588b0212121},
    "<NSMachPort: 0x6000024884d0>"
)
 receivePort == <NSMachPort: 0x60000249c000>
 sendPort == <NSMachPort: 0x6000024884d0>
 msgid == 10010

注意点

  1. handlePortMessage方法
    查看NSMachPortdelegate属性为NSMachPortDelegate,而NSPortdelegate属性为NSPortDelegateNSMachPortDelegate是继承于NSPortDelegate

  2. 子线程运行RunLoop
    调用[[NSRunLoop currentRunLoop] run];的时机不同,就决定是否子线程能够通过NSPort接受到主线程的消息。也就是必须将[[NSRunLoop currentRunLoop] run];一定要放到[self sendPortMessage];之后。

  1. components的类型

    components数组里的内容必须是NSPort或者是NSData
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容