简介
上下文切换 (context switch)指的是内核(操作系统的核心)在CPU上对进程或者线程进行切换;其实际含义是任务切换, 或者CPU寄存器切换
原因
- 当前正在执行的任务完成,系统的CPU正常调度下一个任务。
- 当前正在执行的任务遇到I/O等阻塞操作,调度器挂起此任务,继续调度下一个任务。
- 多个任务并发抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续调度下一个任务。
- 用户的代码挂起当前任务,比如线程执行sleep方法,让出CPU。
- 硬件中断。
一次系统调用的过程,其实是发生了两次 CPU 上下文切换。(用户态->内核态->用户态/同进程内的 CPU 上下文切换)
调度策略
处理器给每个线程分配 CPU 时间片(Time Slice),线程在分配获得的时间片内执行任务,一般为几十毫秒。在这么短的时间内线程互相切换,我们根本感觉不到,所以看上去就好像是同时进行的一样。
就是说,假如同时运行100个线程,CPU为了公平调度,会给每个线程分配时间片,当时间片耗尽之后会立即调度下一个线程(上下文切换)
线程与进程
概念
- 当进程只有一个线程时,可以认为进程就等于线程
- 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。
- 线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的
上下文切换
前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据
解释
就单核系统而言,单位时间cpu能做的事情是固定的,这个上限并不因为使用多线程切换得到提高。
多线程出现的意义,就是为了解决IO和CPU之间速度差的冲突,在IO处理等待的时间,CPU可以去处理其他计算任务。
如果是每个线程一直就是在繁忙的计算,那么多个线程事实上也得不到任何好处,反而因为上下文的切换,要消耗比顺序执行更多的时间。
特别是在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行进程的时间。这也正是导致平均负载升高的一个重要因素。
优化
- 无锁并发编程
- 控制进程/线程数量
- 协程,单进程单线程,内部任务切换
状态监测
pidstat -w -u 5
cswch :表示每秒自愿上下文切换的次数
nvcswch:表示每秒非自愿上下文切换的次数
模拟场景工具
- stress :模拟进程、IO
- sysbench:模拟线程数
在OpenResty的场景举例
推荐配置是worker与CPU保持一致,这里是故意而为之。
由于worker的设计是单进程单线程的,这里我们的例子将模拟使用1个CPU使用2个worker进程
正常的上下文切换
ngx.say("hello")
这句话的意思是输出响应体,如果同时有两个用户进来,worker A和B会争抢CPU资源,假设A抢到了资源,那么B将被挂起,等待A执行完毕之后,CPU将会正常调度下一个任务。
主动的上下文切换
ngx.sleep(0)
以上操作是用来实现主动让出CPU,使用场景是在CPU密集型的程序段,长时间占用CPU,比如以下代码段
for i=10000000000000,1,-1
do
print(i)
end
IO阻塞
os.getenv("SE_UPSTREAMS")
假设在access阶段,每个用户访问它都会从系统环境变量中取值,它就会造成阻塞,需要等待它完成之后CPU才会释放。这个过程经历的两次上下文切换(用户态->内核态->系统态)。可想而知,在高并发环境下,它将会造成阻塞,且会有大量的上下文切换。