iOS多线程(上)

762 阅读8分钟

为了更方便的理解后面的GCD等内容,我们先来介绍一些基本的定义

线程和进程

  • 线程 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行 进程要想执行任务,必须得有线程,进程至少要有一条线程 程序启动会默认开启一条线程,这条线程被称为主线程或UI线程
  • 进程 进程是指在系统中正在运行的一个应用程序 每个进程之间是独立的,每个进程均运行在其专用的且收保护的内存空间内

通过"活动监视器"我们可以查看mac系统中所开启的进程 截屏2021-07-31 上午11.55.36.png 如图所示,一个进程中可以有多条线程。

进程与线程的关系

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

  1. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。(比如我们的mac上有很多应用,但是一个应用奔溃之后,并不会影响别的应用运行,我们的iOS开发的时候,如果线程崩溃,则整个应用程序就会退出。整个进程都会崩溃掉。)
  2. 进程切换时,消耗的资源大、效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
  3. 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  4. 线程是处理器调度的基本单位,但是进程不是。
  5. 线程没有地址空间,线程包含在进程地址空间中。

如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,这样效率就会很低,于是就有了多线程

多线程的意义

优点

  • 能适当提高程序的执行效率
  • 能适当提高资源的利用率(CPU、内存)
  • 线程上的任务执行完成后,线程会自动销毁 缺点
  • 开启线程需占用一定的内存空间(默认情况下,每个线程占512kb)
  • 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
  • 线程越多,cpu在调用线程上的开销就越大
  • 程序设计更加复杂,比如线程间的通信、多线程的数据共享

对于单核的CPU,同一时间,CPU只能处理1个线程,换言之,同一时间只有一个线程在执行。 多线程同时执行其实是CPU快速的在多个线程之间的切换。CPU调度线程的时间足够快,就造成了多线程的"同时"执行的效果。如果要实现真正的并发,还是要多核。 如果线程数量非常多 CPU会在N个线程之间切换,消耗大量的CPU资源。每个线程被调度的次数会降低,线程的执行效率降低。

线程的生命周期

截屏2021-07-31 下午3.08.49.png

  • 新建状态:当程序创建一个线程之后,该线程处于新建状态
  • 就绪状态:当线程对象调用了start()方法之后,该线程处于就绪状态。此时线程等待系统为其分配CPU时间片,并不是说执行了start()就立即执行
  • 运行:就绪状态下的线程在获取CPU资源后就可以执行run(),此时的线程处于运行状态
  • 阻塞:在一个线程执行了sleep、suspend等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态
  • 终止:run()方法完成后或发生其他终止条件时就会切换到终止状态

截屏2021-07-31 下午1.50.05.png

  • 先判断线程池中线程的数量是否超过核心线程数,如果没有超过核心线程数,就创建新的线程去执行任务;如果超过了核心线程数,就进入到下面流程。
  • 判断任务队列是否已经满了,如果没有满,就将任务添加到任务队列中;如果已经满了,就进入到下面的流程。
  • 再判断如果创建一个线程后,线程数是否会超过最大线程数,如果不会超过最大线程数,就创建一个新的线程来执行任务;如果会,则进入到下面的流程。
  • 执行拒绝策略。

拒绝策略

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

任务执行速度的影响因素

  • cpu
  • 任务的复杂度
  • 优先级
  • 线程状态
互斥锁与自旋锁

在多线程情况下会产生一些资源抢夺的情况,这个时候会用到一些互斥锁 互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

临界区:指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。

自旋锁是一种互斥锁的实现方式而已,相比一般的互斥锁会在等待期间放弃cpu,自旋锁(spinlock)则是不断循环并测试锁的状态,这样就一直占着cpu。 对于属性关键字atomic、nonatomic

  • atomic 是原子属性,是为多线程开发准备的,是默认属性! 仅仅在属性的 setter 方法中,增加了锁(自旋锁),能够保证同一时间,只有一条线程对属性进行操作,同一时间 单(线程)写多(线程)读的线程处理技术
  • nonatomic  是非原子属性,没有锁!性能高!

自旋锁与互斥锁的区别:线程在申请自旋锁的时候,线程不会被挂起,而是处于忙等的状态

GCD

全称是 Grand Central Dispatch,纯C语言,提供了非常强大的函数

  • GCD 是苹果公司为多核的并行运算提出的解决方案
  • GCD 会自动利用更多的CPU内核(比如双核、四核)
  • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程) 程序员只需要告诉 
  • GCD 想要执行什么任务,不需要编写任何线程管理代码 GCD将任务添加到队列,并且制定执行任务的函数 任务使用block封装(任务的block没有返回值也没有参数) 执行任务的函数:
  • 异步dispatch_async
  1. 不用等待当前语句执行完毕,就可以执行下一条语句 
  2. 会开启线程执行 block 的任务
  3. 异步是多线程的代名词
  • 同步 dispatch_sync
  1. 必须等待当前语句执行完毕,才会执行下一条语句 
  2. 不会开启线程
  3. 在当前执行 block 的任务
队列
  • 串行队列:说明这个队列中的任务要串行执行,也就是一个一个的执行,必须等上一个任务执行完成之后才能开始下一个,而且一定是按照FIFO的顺序执行的
  • 并发队列:允许你并行执行多个任务。任务开始执行的次序遵照其加入队列的次序。但是,任务执行的过程都同步进行,不需要等待。并发队列保证任务开始执行的次序是确定的,但是你无法知道执行的次序,执行时长或在任意时间点同步执行的任务个数。

我们的队列和函数按照组合可以分为四组:同步串行,同步并发,异步串行,异步并发

看一个🌰

- (void)textDemo2{

    // 并发队列

    dispatch_queue_t queue = dispatch_queue_create("happy", DISPATCH_QUEUE_CONCURRENT);

    NSLog(@"1");

    // 异步函数

    dispatch_async(queue, ^{

        NSLog(@"2");

        // 同步

        dispatch_sync(queue, ^{

            NSLog(@"3");

        });

        NSLog(@"4");

    });

    NSLog(@"5");


}

输出为1,5,2,3,4 不管是执行同步函数还是执行异步函数,都需要消耗时间,所以会先输出1,5 异步函数中先执行2,3是个同步函数,会阻塞4,这个时候4会等待3的执行。

❓我们把dispatch_queue_t queue = dispatch_queue_create("happy", DISPATCH_QUEUE_SERIAL);队列换成同步队列,在运行下呢

就会发生死锁,(左边崩溃函数信息_dispatch_sync_f_slow)

截屏2021-07-31 下午2.56.30.png 由于dispatch_sync是同步的,它就会让3执行,然而3的执行依赖4,就形成了一个死锁。 截屏2021-07-31 下午2.53.36.png 当我们把NSLog(@"4")删除呢 截屏2021-07-31 下午2.43.00.png 还是会死锁,同步阻塞的是外面的块 截屏2021-07-31 下午2.59.20.png