在计算机中,有系统程序和用户程序两种。底层系统程序保持着与底层硬件的一些连接与通信,并且对外提供一些API,例如Windows系统提供的一些能力可以在其官网开发者文档中查到。用户程序一般是无法直接和底层硬件通信的,当需要和底层硬件交互时,可以通过底层系统提供的API接口进行通信。底层系统程序运行的空间被称为内核空间也叫做内核态,用户程序运行的空间被称为用户空间也叫做用户态。
内核态和用户态就像两个门禁系统不同的小区,这样做的好处就是用户程序的崩溃不会影响到内核空间的运行。内核空间可以稳定的运行系统程序对上层提供基础服务。
同步是相对于用户态和内核态的切换,用户态在调用内核态提供的API时,是否需要等待内核完成而言的。阻塞是相对于调用者线程的调用过程。例如用户程序需要加载磁盘中的内容到程序中,调用了read方法。一般的操作是需要内核程序将磁盘导入到内核的IO缓存中,再将内核中的数据拷贝到用户空间去(本文说的都是普通拷贝,零拷贝和mmap等技术可以简化流程,进一步提高运行效率,参考这里),可以看到read的过程会有一定的时间GAP。在数据到达用户空间的这个过程中,用户线程如果需要主动的去监测read方法是否完成则是同步的。如果在调用read方法的时候传入了一个回调函数,然后去做其他的事情,不用主动的去观察是否完成,等待read方法结束后,可以直接执行之前传入的回调函数,或者主动的去通知用户线程说read完成了,这样就是异步的。这里主要涉及用户态和内核态的切换。
阻塞 很好理解,就是调用了一个方法后,是否傻傻等待方法完成,造成CPU空转。如果发现调用了一个方法后(不一定是调用系统API),这个操作比较耗时,需要等待一段时间才能完成,这时候如果可以先去做其他的事情就是非阻塞的,如果只能傻傻地等待方法完成再去做其他的事情则是阻塞的。
主要是同步和异步,大家想象一下用户程序和计算机系统间的关系,在使用计算机的过程中不免会发生调用IO操作,不管是网络IO还是磁盘IO,这些操作都是系统完成的(相比大部分用户空间的程序,这些IO操作是比较耗时的),提供API给用户程序使用,就像是两个不同小区的门禁管理。理想的异步是内核与用户态互不交涉,无缝衔接。就像取快递一样,用户程序发出指令后不需要去门口看看快递有没有来,内核准备好快递后直接送到家里。
上面两张图展示了同步与异步的一个执行流程。在配合上阻塞的操作我们分析下BIO、NIO和AIO的区别。
BIO同步阻塞模型:
BIO模式下,调用完read方法后需要主动地去监听其是否完成,并且在这个过程中无法执行其他的操作。这个过程会造成CPU的空转,浪费系统资源的浪费。
[!NOTE]
注:read方法调用是有可能先于数据到来时操作的。
NIO同步非阻塞模型:
这种模式在BIO的基础上新增了一个轮询操作,允许用户程序注册自己感兴趣的事件,如read事件,并通过轮询去监测数据是否到达,当数据到达后调用相关的API去读写。在没有数据到达的过程中可以去做其他的事情,从而实现了非阻塞的操作,提升了执行效率,避免了CPU在执行耗时IO,或者等待IO数据操作的时候进行空转。但是仍然需要去主动观察内核的执行状态,所以依然是同步的。
AIO异步非阻塞模型:
AIO模型在NIO的基础上更进一步,read方法是异步的,可以立即返回,允许线程去执行其他的业务操作。用户程序只需要提供一个回调方法给内核,内核在数据到达后执行相关的读写操作,在内核工作完成后直接调用之前传入的回调方法,实现了异步非阻塞。上述流程中,①②③均是不同的线程(具体的编程代码可以参考这里),①和②分属用户程序和内核空间,线程不同很好理解。①和③线程的不同证明了异步非阻塞的实现。
可以看到理论上异步效率是要比同步高很多的,但是在实际的实现中却遇到了很多困难,因为系统的API都是公开的,用户程序可以很清晰的知道系统有哪些方法与能力,但是用户程序是多种多样的,系统无法知道用户程序的方法逻辑,在内核执行完后,无法准确的去调用哪一个代码方法,即使传入了回调函数,对于系统来说这个回调也是不可信任的,不能保证其安全性。举个栗子,现在我们想看电影,需要先加载电影内容,然后用播放器播放,加载电影是系统提供的能力,用户 程序去调用,但是系统不知道你具体想用什么播放器播放。所以一般的异步都是内核在完成工作后主动发一个消息给用户空间,用户空间收到内核完成的消息后再去调用之前设置的用户回调程序代码。
Windows平台通过IOCP很好的实现了AIO,IOCP通过与socket绑定,对于socket上发生的IO事件,通知系统线程去处理,处理完成后再丢回用户空间线程执行回调方法,Linux平台下对AIO的支持不是很友好。
补充:
我们经常听说在Java编程中不建议频繁的创建和销毁线程,这是因为Java的线程模型是和内核一一对应的(Jvm是由C++语言实现的),线程是抢占式,并且线程切换需要切换上下文,频繁的创建与销毁会浪费系统资源。具体可以参考这里
jvm.cpp 开启线程的逻辑:
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)){
JavaThread *native_thread = NULL;
native_thread = new JavaThread(&thread_entry, sz);
Thread::start(native_thread)
}
JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
Thread() {
os::ThreadType thr_type = os::java_thread;
os::create_thread(this, thr_type, stack_sz);
}