进程
- 进程是指在系统中正在运行的一个应用程序;
- 每个进程之间是相互独立的,每个进程均运行在其专用的且受保护的内存空间内;
线程
- 线程是进程的基本执行单元,一个进程的所有任务都是在线程中执行的;
- 进程要想执行任务,必须的有线程,一个进程进程至少要有一条线程;
- APP应用程序启动会默认开启一条线程,这条线程被称为 主线程 或者 UI线程;
进程与线程之间的关系
- 进程之间的地址空间是相互独立,不能交叉访问,同一个进程内的线程共享本进程的地址空间;
- 进程之间的资源是相互独立的,同一个进程内的线程共享本进程的资源;
- 两个之间的关系就相当于工厂与流水线的关系,工厂与工厂之间是相互独立的,而工厂中的流水线是共享工厂的资源的,即进程相当于一个工厂,线程相当于工厂中的一条流水线;
线程与RunLoop之间的关系
- RunLoop与线程是一一对应的,其保存在一个全局的字典当中;
- RunLoop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休 眠状态,有了任务才会被唤醒去执行任务;
- 对于主线程来说,RunLoop在程序一启动就默认创建好了,在线程结束时被销毁;
- 对于子线程来说,RunLoop不会默认创建, 其在第一次获取时被创建,所以在子线程用定时器要注意:确保子线程的RunLoop被创建,不然定时器不会回调;
多线程
- iOS中的多线程同时执行的本质是:
CPU在多个任务之间进行快速的切换
,由于CPU调度线程的时间足够快,就造成了多线程的“同时”执行的假象,让我们认为多个线程在同时执行任务,其中切换的时间间隔就是时间片
;
多线程的优缺点
优点:
- 能适当提高应用程序的执行效率;
- 能适当提高系统资源的利用率,如CPU、内存;
- 线程上的任务执行完成后,线程会自动销毁;
缺点:
- 开启线程需要占用一定的内存空间,默认情况下,每一个线程占用512KB,如果开启大量线程,会占用大量的内存空间,降低程序的性能;
- 线程越多,CPU在调用线程上的开销就越大;
- 程序设计更加复杂,比如线程间的通信,多线程的数据共享;
多线程的生命周期
线程的生命周期通常分为5个部分:创建 -- 就绪 -- 运行 -- 阻塞 -- 死亡;其状态之间的切换如下图所示:
- 创建:即实例化线程对象;
- 就绪:线程对象调用start方法,将线程对象加入可调度线程池,等待CPU的调用,即调用start方法,并不会立即执行,而是进入到就绪状态,需要等待一段时间,经CPU调度后才执行;
- 运行:CPU负责从可调度线程池中调度线程然后执行,在线程执行完成之前,其状态可能会在就绪和运行之间来回切换,这个变化是由CPU负责,开发人员不能干预;
- 阻塞:当满足某个预定条件时,可以让线程休眠,即sleep,或者使用同步锁,阻塞线程执行,会将线程从可调度线程池中移除,当线程解除sleep时/获取到锁,会重新将线程加入到可调度线程池中。下面关于休眠的时间设置,都是NSThread的;
- 死亡:分为两种情况
- 正常死亡,即线程执行完毕;
- 非正常死亡,即当满足某个条件后,在线程内部(或者主线程中)终止执行(调用exit方法等退出)
- 简要说明,就是处于运行中的线程拥有一段可以执行的时间(称为时间片),如果时间片用尽,线程就会进入就绪状态队列,如果时间片没有用尽,且需要开始等待某事件,就会进入阻塞状态队列;等待事件发生后,线程又会重新进入就绪状态队列;
- 每当一个线程离开运行,即执行完毕或者强制退出后,会重新从就绪状态队列中选择一个线程继续执行;
- 线程的exit和cancel说明
- exit:一旦强行终止线程,后续的所有代码都不会执行;
- cancel:取消当前线程,但是不能取消正在执行的线程;
线程池的工作原理
- 其工作原理如下图所示:
- 首先线程池有两个重要参数分别为:corePoolSize和maximumPoolSize;
- corePoolSize表示核心线程池能创建核心线程的最大数量;
- maximumPoolSize表示线程池能创建线程的最大数量;核心线程池包含在线程池中。
- 【第一步】:当有任务提交过来,首先判断核心线程池是否已满(corePoolSize)
- 未满,创建核心线程执行任务;
- 已满,进入第二步;
- 【第二步】:判断工作队列是否已满
- 未满,将任务添加到工作队列中;
- 已满,进入第三步;
- 【第三步】:判断线程池是否已满(maximumPoolSize)
- 未满:创建非核心线程执行任务;
- 已满:进入第四步;
- 【第四步】:执行饱和策略,
- 通常有以下四种饱和策略:
- AbortPolicy(抛出一个异常,默认的)
- DiscardPolicy(新提交的任务直接被抛弃)
- DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
- CallerRunsPolicy(交给线程池调用所在的线程进行处理,即将某些任务回退到调用者)
iOS中多线程的实现方案
- 主要有四种分别为:
pthread
,NSThread
,GCD
,NSOperation
- 下面通过代码案例分别演示这四种多线程方案的实现:
【pthread】
//线程回调函数
void * pthreadTest(){
NSLog(@"===>%@", [NSThread currentThread]);
return NULL;
}
- (void)viewDidLoad {
[super viewDidLoad];
pthread_t threadId = NULL;
//c字符串
char *cString = "HelloCode";
//创建一个字线程
int result = pthread_create(&threadId,NULL,pthreadTest,cString);
if (result == 0) {
NSLog(@"pthread 创建成功");
}else{
NSLog(@"pthread 创建失败");
}
}
- 需导入
#import <pthread.h>
【NSThread】
【GCD】
【NSOperation】
线程安全问题
- 在
同一时刻
有多条子线程共同访问
共享资源数据,容易引发数据错乱和数据安全问题,有以下两种解决方案:- 互斥锁(即同步锁):@synchronized
- 自旋锁;
互斥锁
- 用于保护临界区,确保同一时间,只有一条线程能够访问执行;
- 加了互斥锁的代码,当新线程访问时,如果发现有其他线程正在访问共享资源,新线程就会进入休眠状态;
- 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差;
- 能够加锁的任意的NSObject对象;
- 锁对象一定要保证所有的线程都能够访问;
自旋锁
- 自旋锁与互斥锁类似,但它不是通过休眠使线程阻塞,而是在获取锁之前一直处于忙等(即原地打转,称为自旋)阻塞状态;
- 锁持有的时间短,且线程不希望在重新调度上花太多成本时,就需要使用自旋锁,属性修饰符atomic,本身就有一把自旋锁;
- 加入了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用死循环的方法,一直等待锁定的代码执行完成,即不停的尝试执行代码,比较消耗性能;
【面试题】:自旋锁 vs 互斥锁
- 相同点:在同一时间,保证了只有一条线程访问共享资源;
- 不同点:
- 互斥锁:发现其他线程执行,当前线程 休眠(即就绪状态),进入等待执行,即挂起,一直等其他线程打开之后,然后唤醒执行;
- 自旋锁:发现其他线程执行,当前线程一直询问(即一直访问),处于忙等状态,耗费的性能比较高;
- 场景:根据任务复杂度区分,使用不同的锁,但判断不全时,更多是使用互斥锁去处理
当前的任务状态比较短小精悍时,用自旋锁,反之用互斥锁;