为了更方便的理解后面的GCD等内容,我们先来介绍一些基本的定义
线程和进程
- 线程 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行 进程要想执行任务,必须得有线程,进程至少要有一条线程 程序启动会默认开启一条线程,这条线程被称为主线程或UI线程
- 进程 进程是指在系统中正在运行的一个应用程序 每个进程之间是独立的,每个进程均运行在其专用的且收保护的内存空间内
通过"活动监视器"我们可以查看mac系统中所开启的进程
如图所示,一个进程中可以有多条线程。
进程与线程的关系
地址空间: 同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
资源拥有: 同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
- 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。(比如我们的mac上有很多应用,但是一个应用奔溃之后,并不会影响别的应用运行,我们的iOS开发的时候,如果线程崩溃,则整个应用程序就会退出。整个进程都会崩溃掉。)
- 进程切换时,消耗的资源大、效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
- 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 线程是处理器调度的基本单位,但是进程不是。
- 线程没有地址空间,线程包含在进程地址空间中。
如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,这样效率就会很低,于是就有了多线程
多线程的意义
优点
- 能适当提高程序的执行效率
- 能适当提高资源的利用率(CPU、内存)
- 线程上的任务执行完成后,线程会自动销毁
缺点 - 开启线程需占用一定的内存空间(默认情况下,每个线程占512kb)
- 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
- 线程越多,cpu在调用线程上的开销就越大
- 程序设计更加复杂,比如线程间的通信、多线程的数据共享
对于单核的CPU,同一时间,CPU只能处理1个线程,换言之,同一时间只有一个线程在执行。 多线程同时执行其实是CPU快速的在多个线程之间的切换。CPU调度线程的时间足够快,就造成了多线程的"同时"执行的效果。如果要实现真正的并发,还是要多核。 如果线程数量非常多 CPU会在N个线程之间切换,消耗大量的CPU资源。每个线程被调度的次数会降低,线程的执行效率降低。
线程的生命周期
- 新建状态:当程序创建一个线程之后,该线程处于新建状态
- 就绪状态:当线程对象调用了start()方法之后,该线程处于就绪状态。此时线程等待系统为其分配CPU时间片,并不是说执行了start()就立即执行
- 运行:就绪状态下的线程在获取CPU资源后就可以执行run(),此时的线程处于运行状态
- 阻塞:在一个线程执行了sleep、suspend等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态
- 终止:run()方法完成后或发生其他终止条件时就会切换到终止状态
- 先判断线程池中线程的数量是否超过核心线程数,如果没有超过核心线程数,就创建新的线程去执行任务;如果超过了核心线程数,就进入到下面流程。
- 判断任务队列是否已经满了,如果没有满,就将任务添加到任务队列中;如果已经满了,就进入到下面的流程。
- 再判断如果创建一个线程后,线程数是否会超过最大线程数,如果不会超过最大线程数,就创建一个新的线程来执行任务;如果会,则进入到下面的流程。
- 执行拒绝策略。
拒绝策略
- 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
- 不用等待当前语句执行完毕,就可以执行下一条语句
- 会开启线程执行 block 的任务
- 异步是多线程的代名词
- 同步
dispatch_sync
- 必须等待当前语句执行完毕,才会执行下一条语句
- 不会开启线程
- 在当前执行 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)
由于dispatch_sync是同步的,它就会让3执行,然而3的执行依赖4,就形成了一个死锁。
当我们把NSLog(@"4")删除呢
还是会死锁,同步阻塞的是外面的块