美文网首页iOS-多线程
iOS原理 多线程1 -- 多线程的作用和原理

iOS原理 多线程1 -- 多线程的作用和原理

作者: 东篱采桑人 | 来源:发表于2020-11-10 17:35 被阅读0次

    iOS原理 文章汇总

    前言

    在iOS开发中,经常使用多线程来处理一些耗时任务,在学习多线程的实际运用前,需要先了解线程进程以及多线程的原理和概念

    一、线程和进程

    1. 进程
    • iOS开发是单进程的,所以一般来说,一个应用程序就是一个进程
    • 进程之间是相互独立的,每个进程都有独立的内存空间和资源。
    • ⼀个进程崩溃后,在保护模式下不会对其他进程产⽣影响。
    2. 线程
    • 线程是进程的基本执⾏单元,进程的所有任务都必须在线程中执⾏
    • 一个进程⾄少要有⼀条线程,程序启动会默认开启⼀条线程,这条线程被称为主线程或UI线程。
    • 线程没有自己的内存空间,同⼀进程的线程共享本进程的地址空间和资源。
    • 线程不能独⽴执⾏,必须依存在进程中,⼀个线程崩溃整个进程都会死掉。
    3. Mac上的进程和线程示例

    如下图所示,在Mac上,通过“活动监视器”可以查看系统中所开启的所有进程,以及每个进程中运行的线程数。可以看到,每个应用程序都是一个进程,每个进程中会含有多条线程。

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

    二、多线程

    1. 多线程的作用

    进程的任务必须要在线程中执行,程序启动后,所有任务会默认在主线程中执行。如果某个任务比较耗时,就会阻塞主线程,导致程序卡顿,这时就需要再创建一个子线程来处理这个耗时任务,来缓解主线程的压力。相比单线程,多线程的优缺点如下:

    优点
    1.能适当提⾼程序的执⾏效率;
    2.能适当提⾼资源的利⽤率(CPU,内存);
    3.线程上的任务执⾏完成后,线程会⾃动销毁。

    缺点
    1.开启线程需要占⽤⼀定的内存空间(默认情况下,每⼀个线程都占 512 KB),线程越多,占⽤的内存空间越大,程序性能就越低。
    2.任务需要由CPU调度线程来执行,线程越多,CPU 在调⽤线程上的开销就越⼤,每个线程被调度的次数会降低,线程的执⾏效率也会降低。
    3.程序设计更加复杂,⽐如线程间的通信、多线程的数据共享。

    2. 多线程的原理

    一个进程的全部任务,是需要通过CPU调度线程来进行处理的。当进程中存在多个线程时,CPU会在极短的时间间隔内快速调度不同的线程来处理这些任务,这个时间间隔也被称为『时间片』。

    CPU分为『单核CPU』和『多核CPU』,它们调度线程的能力也有所不同:

    • 单核CPU:同⼀时间,CPU 只能调度1个线程。
    • 多核CPU:同一时间,CPU可以调度多个线程。

    虽然这些线程实际上并不是同时执行,但由于CPU调度线程的时间足够快,也就造成了多线程”同时“执⾏的效果。

    CPU调度线程
    3. 多线程的生命周期

    多线程的生命周期分为5个状态:新建就绪运行阻塞死亡,流程如下图所示:

    • 新建:创建一个线程。
    • 就绪:线程创建后,调用start方法,就会被添加到『可调度线程池』,等待CPU调度,此时为就绪状态(Runnable)。
    • 运行:线程被CPU调度后,会被移出『可调度线程池』,成为运行状态(Running)。
    • 阻塞:线程变为运行状态后,可能会遇到一些情况比如:线程sleep、同步锁,导致不能立即执行,变为阻塞状态。此时线程会休眠(互斥锁会休眠,自旋锁会忙等轮询),等到同步锁或者线程sleep结束后,会再被添加回『可调度线程池』,又变为就绪状态。
    • 死亡:线程变为运行状态后,若没有遇到同步锁或线程sleep,就被立即执行,执行完后,线程结束,会被销毁,即成为死亡状态。如果线程被提前终止执行,比如调用exit方法退出执行,也会被销毁变为死亡状态。
    4. 线程池原理

    线程池的处理逻辑如图所示,先来认识三个概念:

    • corePoolSize:核心线程池大小,即线程池的基本大小。
    • maximumPool:线程池的最大大小。
    • keepAliveTime:线程池中超过corePoolSize数目的空闲线程的最大存活时间。

    当一个新任务到来时,线程池的处理逻辑如下:

    • step1:判断当前线程池大小是否小于核心线程池大小
      Yes:创建线程去执行任务。
      No:进入step2。

    • step2:判断线程池工作队列是否已满
      Yes:进入step3。
      No:提交任务到工作队列,等待CPU调度线程执行。

    • step3:判断线程池中的线程数是否小于最大数量
      Yes:创建新的线程来执行任务。
      No:进入step4。

    • step4:交给饱和策略处理,主要有以下四种处理方案:

      • AbortPolicy:直接抛出RejectedExecutionExeception异常来阻⽌系统正常运⾏。
      • CallerRunsPolicy:将任务回退到调⽤者。
      • DisOldestPolicy:丢掉等待最久的任务。
      • DisCardPolicy:直接丢弃任务。
    5. 线程安全问题

    线程共享进程的地址空间和资源,因此当多个线程同时访问一块资源时,容易引发数据安全问题。为了保证多线程的数据读写安全,一般会对线程进行加锁处理,分为『互斥锁』和『自旋锁』两种。这两种锁均能保证锁内的代码,同⼀时间,只有⼀条线程能够执⾏

    互斥锁
    • 当线程遇到互斥锁时,会变为休眠状态,等锁里的线程执行完,才会被唤醒然后添加到『可调度线程池』,再等待CPU调动执行。
    • 互斥锁有这几种:@synchronized, NSLock, pthread_mutex, NSConditionLock, NSCondition, NSRecursiveLock
    自旋锁
    • 当线程遇到自旋锁时,不会休眠,会变为忙等待(busy waiting)的状态,并轮询检查锁是否可用,等锁里的线程执行完任务,会立即开始执行。
    • 自旋锁有这几种:atomic, OSSpinLock, dispatch_semaphore_t

    一般来说,当锁里的任务能较短时间处理完,使用自旋锁性能更好。反之,使用互斥锁。

    atomic & nonatomic

    atomicnonatomic都是用来修斯属性的,atomic表示原子属性,nonatomic表示非原子属性,它们的区别在于:

    • atomic针对多线程设计的,实际是在属性的setter方法里加了一个自旋锁,保证同⼀时间只有⼀个线程能够写⼊,使其修饰的属性只能多读单写。虽然能保证线程安全,但需要消耗大量的资源。
    • nonatomic修饰的属性是可以多读多写的,不保证线程安全,但效率高。
    • 在iOS开发中,属性默认是atomic修饰的,但是建议都声明为nonatomic,因为效率相差近10倍。

    三、多线程的实现方案

    在iOS中,多线程实现方式主要有这四种:pthreadNSThreadGCDNSOperation,如图所示:

    由于pthread现在已几乎不用了,这里就只介绍另外三种方案的用法了。下面是对一个耗时任务分别以单线程和多线程处理来作示例:

    • 耗时任务阻塞主线程
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // Do any additional setup after loading the view.
        
        //task1
        NSLog(@" ==== task1");
        
        //task2
        [self threadTest];
        
        //task3
        NSLog(@" ==== task3");
    }
    
    //耗时任务
    - (void)threadTest{
      
        NSLog(@" ==== begin task2");
        NSInteger count = 50000;
        for (NSInteger i = 0; i < count; i++) {
    
            NSInteger num = I;
            NSLog(@" ==== num = %ld", num);
        }
        NSLog(@" ==== end task2");
    }
    
    //打印结果
    2020-11-05 16:10:40.577616+0800 Test[71508:23639447]  ==== task1
    2020-11-05 16:10:40.577703+0800 Test[71508:23639447]  ==== begin task2
    2020-11-05 16:10:48.117782+0800 Test[71508:23639447]  ==== end task2
    2020-11-05 16:10:48.117894+0800 Test[71508:23639447]  ==== task3
    

    这个例子中,主线程中有task1、task2、task3这三个任务需要执行(这里暂把task2当成一个任务),其中task2是个耗时任务,需要执行8s,而task3其实并不依赖task2的执行结果,但还是需要等待task2结束后才开始执行,所以对于task3来说,task2造成了线程阻塞。像这种情况,完全可以创建一个子线程来执行task2,这样主线程可以立即执行task3,不用再耗时等待了。

    • 多线程处理任务
    //分别用三种方式创建子线程来处理task2
     -(void)performThreadTest{
        
        //1.NSThread
        [NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
        
        //2.GCD
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
            [self threadTest];
        });
    
        //3.NSOperation
        [[[NSOperationQueue alloc] init] addOperationWithBlock:^{
            
            [self threadTest];
        }];
    }
    
    //打印结果
    2020-11-05 16:12:03.541448+0800 Test[71597:23651072]  ==== task1
    2020-11-05 16:12:03.541570+0800 Test[71597:23651072]  ==== task3
    2020-11-05 16:12:03.545564+0800 Test[71597:23651434]  ==== begin task2
    2020-11-05 16:12:12.445006+0800 Test[71597:23651434]  ==== end task2
    

    从打印结果可知,在子线程中执行task2,这样主线程就不会被阻塞,可以立即执行task3。所以,当进程中存在耗时任务时,创建子线程来执行这个耗时任务,可以缓解主线程的压力,防止线程被阻塞,提高任务的处理效率

    四、线程间通讯

    在iOS中,线程间的通讯主要有下图所示的几种方式,详情可查阅苹果开发文档Threading Programming Guide

    • 直接消息传递:通过performSelector的一系列方法,可以实现由某一线程指定在另外的线程上执行任务。因为任务的执行上下文是目标线程,这种方式发送的消息将会自动的被序列化。

    • 全局变量、共享内存块和对象:在两个线程之间传递信息的另一种简单方法是使用全局变量,共享对象或共享内存块。尽管共享变量既快速又简单,但是它们比直接消息传递更脆弱。必须使用锁或其他同步机制仔细保护共享变量,以确保代码的正确性。 否则可能会导致竞争状况,数据损坏或崩溃。

    • 条件执行:条件是一种同步工具,可用于控制线程何时执行代码的特定部分。您可以将条件视为关守,让线程仅在满足指定条件时运行。

    • Runloop sources:一个自定义的 Runloop source 配置可以让一个线程上收到特定的应用程序消息。由于 Runloop source 是事件驱动的,因此在无事可做时,线程会自动进入睡眠状态,从而提高了线程的效率。

    • Ports and sockets:基于端口的通信是在两个线程之间进行通信的一种更为复杂的方法,但它也是一种非常可靠的技术。更重要的是,端口和套接字可用于与外部实体(例如其他进程和服务)进行通信。为了提高效率,使用 Runloop source 来实现端口,因此当端口上没有数据等待时,线程将进入睡眠状态。需要注意的是,端口通讯需要将端口加入到主线程的Runloop中,否则不会走到端口回调方法。

    • 消息队列:传统的多处理服务定义了先进先出(FIFO)队列抽象,用于管理传入和传出数据。尽管消息队列既简单又方便,但是它们不如其他一些通信技术高效。

    • Cocoa 分布式对象:分布式对象是一种 Cocoa 技术,可提供基于端口的通信的高级实现。尽管可以将这种技术用于线程间通信,但是强烈建议不要这样做,因为它会产生大量开销。分布式对象更适合与其他进程进行通信,尽管在这些进程之间进行事务的开销也很高。

    相关文章

      网友评论

        本文标题:iOS原理 多线程1 -- 多线程的作用和原理

        本文链接:https://www.haomeiwen.com/subject/caowvktx.html