20210819补充:线程阻塞(“阻塞”可以是指线程阻塞,也可以是指IO阻塞,两者不是一件事)的本质,详情可参阅文章 线程阻塞的本质

深入到Linux内核源码看,阻塞与非阻塞的最终效果是线程状态的改变——阻塞就是将当前线程的状态标记为非RUNNABLE状态(例如Java scanner.nextLine() 最终在Linux层面就是将当前线程标为TASK_INTERRUPTIBLE状态),这样进程调度(最终是线程调度)时该线程就不会被分配CPU执行权限,从而看上去是"阻塞"了;

相应地,线程唤醒的最终效果就是将该线程状态变为RUNNABLE(例如上述例子中用户在终端输入字符时,就会产生中断,中断程序的执行效果是改变线程状态),这样该线程就可以被调度器分配CPU执行权限,看上去就是唤醒了。

 

下面所说的“阻塞”指IO阻塞

 

几个概念:

IO:以内存为参照物,数据在内存与外设(网卡、磁盘等)间的输入Input和输出Output。包括网络IO、文件IO。

IO事件:IO过程中的一些状态,如建立连接、接受连接、数据可读(数据复制到内存缓冲区完成)、数据可写(数据写到内存缓冲区完成)等。

IO密集型、CPU密集型任务:程序操作的主要时间花在外设和内存间数据读写时属于IO密集型操作,如网络数据收发、文件读写等;主要时间花费在CPU计算的属于CPU密集型操作,如矩阵乘法、大数的因数分解等。

多线程/多进程处理:通常通过他们来并发执行一批任务。然而多线程是否一定能提高执行效率呢?并没有固定的结论。

从CPU核数看,如果CPU有多核,那么用多线程执行一批任务显然比用单线程的执行效率高(不论是CPU密集型还是IO密集型任务)。

从任务类型看,假设CPU只有一核(控制变量法..):

对于CPU密集型任务,这些多线程任务本质上是在CPU上串行执行的(只不过每个线程任务都不是连续执行完而是被分配多个时间片交叉轮流执行),因此整体效率并不比单线程执行快。但是多线程使得在用户看来这多个任务是并发执行的从而不用等待上一个任务完成再执行下一任务,因此此类型任务虽然整体效率不见得比单线程高但对用户体验来说是值得的。

对于IO密集型任务,在数据IO的第一阶段(见下文IO内部机制部分)时用户线程是要阻塞的,即不占用CPU,因此此时能把CPU让给其他线程进行IO而不是空占CPU。可见,此类型任务下多线程可比单线程效率高。

综上,现代CPU都有多核了,因此整体而言一批任务用多线程执行比单线程执行的效率高。当然,一切都不是绝对的——线程数过多带来的线程创建、调度、上下文切换等的时间、空间开销可能超过多线程带来的效率优势,因此,要结合具体的需求和场景分析。

IO操作的姿势:同步(synchronous)IO、异步(asynchronous)IO、阻塞(blocking)IO、非阻塞(nonblocking)IO

 

出于安全考虑,用户程序(用户态)是没办法直接操作IO设备进行数据读入或输出的,需要借助操作系统(内核态)提供的API来进行IO,所以通常我们说IO其实是通过系统调用来完成的。(关于IO的具体过程,可参阅 https://www.cnblogs.com/z-sm/p/15163921.html

程序发起IO调用时涉及两个阶段,以read为例:

  1. 数据准备阶段:等待内核态将数据从外设读入内核态内存并准备好,进入就绪状态 (Waiting for the data to be ready)。这步是外设与内核态内存间的复制,是耗时操作!
  2. 数据复制阶段:将数据从内核态复制到用户态即从内核态内存复制到用户态内存 (Copying the data from the kernel to the process)。这步是内存间的复制,比上步快很多。

通常说的IO“阻塞”是指在上述步骤1过程中用户调用者线程是否阻塞。

 

2、同步、异步、阻塞、非阻塞IO的区别

阻塞、非阻塞IO(针对系统调用(内核态)而言?):在于发起的IO调用是否立即返回。阻塞IO等IO完成才返回(即等1、2都结束才返回。1、2均阻塞),非阻塞IO立即返回,此时IO还没完成(即1立即返回,若数据没准备好则循环检测直到就绪,就绪后等阶段2。1不阻塞、2阻塞)。可见,阻塞非阻塞的的区别体现在是否等待耗时步骤1完成。

一个不那么恰当的比喻:假设你在深圳家里,你朋友在北京要来找你且刚出发,你要去深圳车站接你朋友。你朋友到车站前这段过程、从车站到你家这段过程分别相当于上述阶段1、2。阻塞IO:你此时就去车站接你朋友,显然需要等很久;非阻塞IO:你告诉你朋友要去车站接他,让他到车站时跟你说你再去车站。

同步、异步IO(针对用户线程(用户态)而言?):在于调用者(线程)在发起IO调用后在IO完成前能否继续执行之后的代码或工作。

阻塞不一定是同步的,非阻塞也不一定是异步的,反之亦然。

  • 同步阻塞IO:如JDK IO(发起IO调用不立即返回,调用者在IO完成前也没法进行之后的操作)。
  • 同步非阻塞IO:如JDK NIO(发起IO调用后立即返回,此后通过循环检查等手段直到IO就绪才进行IO操作,操作完了才进行之后的工作,因此是同步的)。
  • 异步IO:如可以写一个带回调参数的方法,该方法启用新线程进行IO——根据String content、String filePath参数将conent写入指定文件,完成后调用回调函数通知调用者。至于是阻塞还是非阻塞则看新线程内进行的IO是阻塞还是非阻塞的。一个异步阻塞IO的示例如下:
    IO模型(同步、非同步、阻塞、非阻塞IO)总结
     1 public class FileIO {
     2     public void saveStrToFile(String fileName, String str, IFileIOCallback callback) {
     3         new Thread(new Runnable() {
     4             @Override
     5             public void run() {
     6                 try {
     7                     File file = getExistsFile(fileName);
     8                     writeStrToFile(str, file);
     9                     callback.onResult(true);
    10                 } catch (IOException e) {
    11                     e.printStackTrace();
    12                     callback.onResult(false);
    13                 }
    14             }
    15         }).start();
    16     }
    17 }
    View Code

相关文章: