简介

上下文切换 (context switch)指的是内核(操作系统的核心)在CPU上对进程或者线程进行切换;其实际含义是任务切换, 或者CPU寄存器切换

原因

  • 当前正在执行的任务完成,系统的CPU正常调度下一个任务。
  • 当前正在执行的任务遇到I/O等阻塞操作,调度器挂起此任务,继续调度下一个任务。
  • 多个任务并发抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续调度下一个任务。
  • 用户的代码挂起当前任务,比如线程执行sleep方法,让出CPU。
  • 硬件中断。

一次系统调用的过程,其实是发生了两次 CPU 上下文切换。(用户态->内核态->用户态/同进程内的 CPU 上下文切换)

调度策略

处理器给每个线程分配 CPU 时间片(Time Slice),线程在分配获得的时间片内执行任务,一般为几十毫秒。在这么短的时间内线程互相切换,我们根本感觉不到,所以看上去就好像是同时进行的一样。

就是说,假如同时运行100个线程,CPU为了公平调度,会给每个线程分配时间片,当时间片耗尽之后会立即调度下一个线程(上下文切换)

线程与进程

概念

  1. 当进程只有一个线程时,可以认为进程就等于线程
  2. 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。
  3. 线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的

上下文切换
前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。

前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据

解释

就单核系统而言,单位时间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才会释放。这个过程经历的两次上下文切换(用户态->内核态->系统态)。可想而知,在高并发环境下,它将会造成阻塞,且会有大量的上下文切换。