IO 阻塞

昨天在整理自己的博客的时候,想到的一个问题:进程在从磁盘中读取内容的时候,CPU 在干什么???

之所以会这样问,是因为,CPU的速度比磁盘要快很多,具体有多块,看这篇文章。

磁盘寻址时间为 10ms,换算成人类时间是 10个月,刚好够人类创造一个新的生命了。如果 CPU 需要让磁盘泡杯咖啡,在它眼里,磁盘去生了个孩子,回来告诉它你让我泡的咖啡好了。

这是从文中截取的一段,可以体会一下。

那么我的问题的本质是,在这段等带的时间中,CPU 是去搞别的是事情了(执行别的线程),还是空等呢???IO 阻塞与线程遇到锁产生的阻塞是一样的吗?

经过查询资料,Linux系统下,CPU与磁盘交互时大致发生了如下事情:

  • 进程调用库函数向内核发起读文件请求(切换到内核态);

  • 在 Page Cache(它位于内存和文件之间缓冲区,文件IO操作实际上只和page cache交互,不直接和内存交互。) 中查询数据是否存在。

  • 如果页缓存命中,那么直接返回文件内容;如果页缓存缺失,那么产生一个页缺失异常,需要从磁盘读取数据。

  • CPU向磁盘发送数据读取指令。

  • 然后,发送完指令后,CPU会转去执行其它任务(为了提高效率),磁盘则会将逻辑块号转换成对应的盘片、磁道、扇区组成的三元组,从而定位到了数据所在的扇区。之后磁盘会采用DMA(直接存储器访问技术,其不需要CPU干预)传送数据到CPU指定的主存地址。

  • 最后,磁盘传送完毕后,会直接发送一个中断信号给CPU芯片的一个外部引脚,把CPU“召唤”回来重新执行先前未完成的任务。

上面的过程不一定准确,但是比较符合我的期望了,准备恶补一下计算机的相关原理。

所以,在 IO 阻塞的时间之内,CPU 也没有闲着(反过来想一下,如果CPU无法干别的事情的话,那么只要IO线程大于CPU线程,那么就卡死了)。

线程状态

其实这里面也有提到,我之前没有注意。

IO 阻塞

线程的状态分为:

  1. 可运行(就绪):线程被创建之后,调用Start()函数就到了这个状态。

  2. 运行:Start()函数之后,CPU切换到了这个线程开始执行里面的Run方法就称为运行状态。

  3. 阻塞:阻塞状态是指线程因为某种原因放弃了cpu执行权,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu 执行权 转到运行(running)状态。阻塞的情况分三种。

  • 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。

  • 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

  • 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

  1. 结束

    线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

线程的个数

一般说来,大家认为线程池的大小经验值应该这样设置:(其中N为CPU的个数)

  • 如果是CPU密集型应用,则线程池大小设置为N+1
  • 如果是IO密集型应用,则线程池大小设置为2N+1

我们简单的思考一下,为什么会这样???

对于CPU密集型任务,尽量使用较小的线程池,因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销

IO密集型任务
可以使用稍大的线程池,IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。

但是,其实最让我疑惑的是,为何要 +1?

然后我看到了这句话:

即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的时钟周期不会被浪费。

额,这里节省的时间能够补回来由于多了一个线程导致的线程切换的开销吗???