线程和进程
进程
进程是指在系统中正在运行的一个应用程序
每个进程之间是独立的,每个进程运行在其专用的且受保护的内存空间内
通过活动监视器
可以查看mac系统中所有开启的进程
进程没有直接执行任务的能力
线程
即 thread
是操作系统能够进行运算调度的最小单位。被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务; 线程是进程中的实际执行单元,进程中的任务执行依赖线程来实现
线程和进程的关系:
线程是进程中的实际执行单元,进程中的任务执行依赖线程来实现, 例如: 生产车间和工人的关系, 生产车间负责工作环境的准备 工人来实际生产产品
线程各个状态下线程所处的位置:
新建状态
:线程被创建 在内存中,但不在可调度池
就绪状态
:线程对象调用 start 函数 将线程加入可调度线程池, 等待CPU的调用; 即调用start函数并不会立即执行,进入就绪状态后 需要等待CPU调度
运行状态
:CPU负责调度线程池中线程执行任务, 在线程执行完成之前 , 其状态可能会在就绪和运行之间来回切换,这个变化是由CPU负责,开发人员不能干预
阻塞状态
:当满足某个预定条件时 阻塞线程执行(比如: 同步锁/sleep()), 当进入sleep时线程会再次进入就绪状态 ;
NSThread的休眠:
sleepUntilDate: 阻塞当前线程,直到指定的时间为止 即休眠到指定时间
sleepForTimeInterval: 在给定的时间间隔内休眠 , 即指定休眠时长
同步锁: @synchronized(self);
死亡状态
:
正常死亡: 线程执行完毕
非正常死亡: 当满足某个条件后,在线程内部(或者主线程)终止执行(调用exit) 或异常崩溃
线程的exit和cancel :
exit
: 强行终止线程, 后续的所有代码都不会执行; 很有可能引起数据错乱
cancel
: 取消当前线程,但不能取消正在执行的线程
线程池原理
第一步: 判断核心线程池是否都在执行任务
返回NO ,创建新线程去执行任务
返回YES ,进入第二步
第二步: 判断线程池工作队列是否已经饱和
返回NO, 将任务存储到工作队列,等待CPU调度
返回YES,进入第三步
第三步:判断线程池中的线程是否都处于执行状态
返回NO,安排可调度线程池中空闲的线程去执行任务
返回YES,进入第四步
第四步:交给饱和策略去执行,主要有四种(在iOS中并没有找到以下4种策略)
AbortPolicy: 直接抛出RejectedExcutionExeception
异常来阻止系统正常运行
CallerRunsPolicy: 将任务退回到调度者
DisOldestPolicy: 丢掉等待最久的任务
DisCardPolicy: 直接丢弃任务
什么是主线程?
当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),因为它是程序开始时就执行的。主线程是负责执行main函数的线程;主线程中几乎所有的事情都是交给runloop去做,比如UI界面刷新、点击事件的处理、performSelector等需要Runloop,但是像简单的普通代码:NSLog输出、变量定义等是不需要Runloop参与的;
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
线程间通信
在Thread Programming Guide文档中,提及线程进通讯有以下几种方式
-
直接消息传递
: 通过performSelector:onThread:
的一些列方法可以实现由某一线程之指定在另外的线程上执行任务; 因为任务的执行上下文是目标线程,这种方式发送消息将会自动的被序列化 -
全局变量/共享内存块和对象
: 在两个线程之间传递信息的另一种简单方法是使用全局变量,共享对象或共享内存块 ,尽管共享变量即快速又简单,但是他们比直接消息传递更脆弱, 容易有线程安全问题必须使用锁或其他同步机制仔细保护共享变量 以确保代码的正确性
否则可能会导致资源竞争引起数据错乱或崩溃
-
条件执行
: 条件是一种同步工具
,可用于控制线程何时执行代码的特定部分, 可以将条件视为关守 让线程仅在满足指定条件时运行 -
Runloop sources
: 一个自定义的Runloop source配置可以让一个线程收到的定的应用程序消息; 由于Runloop source是事件驱动
的,因此在无事可做时 线程会自动进入休眠状态
从而提高线程的效率 -
Ports add sockets
: 基于端口的通信
是在两个线程之间进行通信的一种更为复杂的方法, 但它也是一种非常可靠的技术
, 更重要的是 端口和套接字可用与外部实体(例如其他进程和服务)进行通信; 为了提高效率使用Runloop source来实现端口, 因此当端口上没有数据等待时 线程将进入休眠状态; 需要注意的是 端口通讯需要将端口加入到主线程的runloop中
,否则不会走到端口回调方法 -
消息队列
: 传统的多处理服务定义了先进先出(FIFO)队列抽象,用于管理传入和传出数据 尽管消息队列即简单又方便 但是它们不如其他一些通信技术高效 -
Coca分布式对象
: 分布式对像是一种Cocoa技术,可提供基于端口的通信的高级实现,尽管可以将这种技术用于线程间通信但是强烈建议不要这样做,因为它会产生大量开销; 分布式对象更适合与其他进程间通信,尽管在这些进程之间进行事务的开销也很高
线程的优先级越高是不是意味着任务执行的越快 ?
并不是的,线程执行的快慢受两方面影响:
- 任务的复杂度/CPU调度情况
- 优先级(priority) 线程的优先级(threadPriority)
-
QoS
(quality of service)意指服务质量
QoS
QoS(quality of service) 指线程的服务质量,他影响线程的优先级(priority), 也影响I/O吞吐/CPU吞吐等指标; 开发者可以用 qos_class_self
获取当前线程/队列的QoS
Table 4-5GCD to Foundation QoS mappings 相关枚举
苹果对于每个任务应该选用那个Qos也有一些指导性意见:
Table 4-1Primary QoS classes (shown in order of priority)
QoS 和 priority 是有对应关系的,参考 xnu 源码和实验结果,对应关系为:
同时,线程的 priority 会随着执行动态调整。测试中我们会发现,主线程的 priority 在运行开始时是 QoS User-Interactive 对应的 47,但随着运行会出现下降的情况。
官方文档中解释了线程 priority 变化的原因,priority 由 Mach scheduler
控制,为了防止计算密集的线程垄断资源,各个线程的 priority 会实时调整。
XNU 内核的源码中, 线程 priority 的变化,是由各个 Mach scheduler 实现的 compute_timeshare_priority
接口控制的。在 iOS 使用的 Mach scheduler 中,compute_timeshare_priority
为同一个实现 sched_compute_timeshare_priority
。线程调度时的 priority,会在线程固有 priority 的基础上,结合当前线程的 CPU 占用情况和当前设备的整体负载进行调整
在这个实现中,我们能看到 Mach scheduler 对 priority 的调整会有一个极限:对于原先 priority = 47 的线程来说,向下调整的极限是 47 - ((BASEPRI_FOREGROUND - BASEPRI_DEFAULT) + 2) = 29。这和我们用多个设备测试到的结果吻合:主线程执行时,priority 的最低值是 29,依然高于 Utility 对应的 priority 20, 这也证明了当异步线程的 QoS 是Utility 时,就几乎无法对主线程造成抢占 ! 利用此思路可以对启动时间进行优化,尤其是低端机效果明显, 核心思路: QoS 是 User-Initiated的时候,尽管这一 QoS 低于主线程的 User-Interactive,但依然可能对主线程造成抢占;需要将异步队列的 QoS 调低到 Utility
不改一行业务代码,飞书 iOS 低端机启动优化实践
一条线程占多大内存空间 ?
在iOS中主线程占1MB 子线程占512KB ????
还是代码测试一下吧 !!
//子线程内存大小
- (void) getstacksize {
size_t stack_size = 0; //堆栈大小变量
pthread_attr_t attr; //线程属性结构体变量
//初始化线程属性
int ret = pthread_attr_init(&attr);
if(ret != 0)
{
perror("pthread_attr_init");
return;
}
//获取当前的线程栈大小
ret = pthread_attr_getstacksize(&attr, &stack_size);
if(ret != 0)
{
perror("pthread_attr_getstacksize");
return;
}
//打印堆栈值 stack_size = 524288B, 512k
printf("stack_size = %zuB, %luk\n", stack_size, stack_size/1024);
}
//MARK: 主线程的内存大小
- (void)test36 {
NSThread *main = [NSThread currentThread];
NSLog(@"%@",main);// 打印结果 {number = 1, name = main}
NSUInteger size = main.stackSize;
printf("stack_size = %ldB, %ldk\n", size, size/1024);//stack_size = 524288B, 512k
}
总结:线程默认给的是512KB的内存, 可以通过pthread_attr_setstacksize
设置线程的内存
pthread_attr_getstacksize
查询线程内存
OC可以通过NSThread的setStackSize来设置
线程的调度方式是什么 ?
先说下时间片
- 时间片是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间,时间片的大小对系统的性能影响很大。
如果时间片足够大,以至于所有进程都能在一个时间片内执行完毕,则时间片轮转调度算法就退化为先来先服务调度算发。如果时间片很小,那么处理机将在进程间过于频繁切换,使处理机的开销增大,而真正用于处理用户作业的时间将减少,因此时间片的大小应选择适当。
线程被放在线程池
里等待CPU来调度, 常见的CPU调度算法
先来先服务算法(FCFS)
这个调度算法是最简单的调度算法,这个算法按照每次在就绪队列中选择最先进入该队列的进程进行调度。属于不可剥夺度算法,有利于CPU繁忙型作业,不利于I/O繁忙性作业。短作业优先算法(SJF)
此算法是从后背队列中选择一个或几个估计运行时间短的作业,优先调度到内存中运行。该做法对场作业不利,所以无法保证紧迫性的作业会被及时处理。优先级调度算法
分为非剥夺和剥夺式的调度。每个线程有一个优先级,CPU每次去拿优先级高的运行,优先级低的等等,为了避免等太久,每等一定时间,就给线程提高一个优先级。高响应比优先调度算法
是对先来先服务算法(FCFS)和短作业优先算法(SJF)的一种综合平衡,克服了饥饿状态也兼顾了场作业。时间轮转片调度算法
主要适用于分时系统。根据先来先服务的原则,但是仅能运行一个时间片,用完之后及时没有完成运行任务,也要将资源释放给下一个就绪作业,被剥夺的返回就绪队列直至下一次被运行。多级反馈队列调度算法
线程池有几种 ?
线程池 是一种多线程处理形式(或模式),处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务
一般线程池由部分组成:
-
线程池管理器
: 用于创建并管理线程池 -
工作线程
:线程池中的线程 -
任务接口
:每个任务必须实现的接口,用于工作线程调度其运行 -
任务队列
:用于存放待处理的任务,提供一种缓冲机制
可以通过java中的六种线程池了解线程池的工作方式
OC中的线程池管理器: NSConnection
是用于管理不同线程中的对象之间的通信,或者在本地或远程系统上运行的线程与进程之间的通信
如何停止一个线程 ?
- 首先不能简单的停止一个线程, 因为停止一个线程会直接把线程干掉,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,可能会导致数据完整性问题
- 虽然线程不能在中间被停止/干掉,但是任务是可以停止的; 想让线程结束的目的是让任务结束,而不是强制线程结束(exit); 有两种方式结束任务,分别是Interrupt和boolean标志位
- 1.使用线程中断机制-interrupt停止线程, 原生支持sleep、wait等可以让线程进入阻塞状态使线程休眠, 而处于休眠中的线程被中断,那么线程是可以感受到中断信号的
- 2.使用volatile boolean标志位停止线程:线程中设置一个boolean标志位值为false,线程里不断读取这个boolean值,其他地方可以修改这个boolean值;为了保证内存可见性,给boolean标志位添加volatile保证可见性;当某一个线程修改boolean标志位为true,线程中能立刻看到。
如何选择interrupt和boolean标志位去停止线程?interrupt()和boolean标志位的原理是一致的。除非是用到了系统方法时(如:sleep) 或者 使用阻塞队列发生阻塞,使用interrupt();否则,建议使用boolean标志位,性能更优,毕竟interrupt有一定的开销
这道题想考察什么?
考察要点 :
- 是否对线程的用法有了解;是否对线程的exit方法有了解(初级)
- 是否对线程exit过程中存在的问题有认识;是否熟悉interrupt中断的用法(中级)
- 是否能解释清楚使用boolean标志位的好处;是否知道interrupt底层的细节;通过该题目能够转移话题到线程安全,并阐述无误(高级)
exit官方文档解释:
- 终止当前线程
- 这个方法使用currentThread类方法来访问当前线程。在退出线程之前,这个方法将退出线程的NSThreadWillExitNotification发送到默认通知中心。因为通知是同步发送的,所以NSThreadWillExitNotification的所有观察者都保证在线程退出之前接收到通知。
- 应该避免调用此方法,因为它不会给您的线程一个机会来清理它在执行期间分配的任何资源。
线程安全方案:
- 对访问过程加锁
- 用线程同步技术
附: 多线程安全方案