Loading

Python并发编程之IO模型 同步 异步 阻塞 非阻塞

IO浅谈

首先

我们在谈及IO模型的时候,就必须要引入一个“操作系统”级别的调度者-系统内核(kernel),而阻塞非阻塞是跟进程/线程严密相关的,而进程/线程又是依赖于操作系统存在的,所以自然不能脱离操作系统来讨论阻塞非阻塞。同步/异步也是跟任务流相关的,所以要全面理解就必须考虑到并发的任务流,不然,肯定很难举出恰当的例子的。

 

本文讨论的背景是Linux环境下的network IO。
本文最重要的参考文献是Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models ”,Stevens在这节中详细说明了各种IO的特点和区别,如果英文够好的话,推荐直接阅读。Stevens的文风是有名的深入浅出,所以不用担心看不懂。本文中的流程图也是截取自参考文献。

 

在网络并发编程中,通俗的讲IO分为两个东西:1、等待 ==> 2、数据搬迁

或者这样说:当IO发生时,会涉及到对象和步骤,当进行网络IO(Network IO)时候,必须涉及到两个系统对象,一个不可避免的就是产生这个IO的进程或者线程(process or thread),还有另外一个老大级人物:系统内核(kernel),至于为何要让老大出场处理IO,究其原因就是,比如我们在进行读操作的时候,进程或线程是无法直接跟硬件打交道,无法直接从硬盘中直接拿到我们想要的数据,而是需要系统内核取帮我们去取,它取需要过程,有过程我们就得等待,系统内核取到了先会将数据放进它自己的内存空间中,我们要取还得从它的内存空间把数据迁移到进程的内存空间,这才算真正地拿到数据。

通俗的讲就是:

当一个read操作发生时,会经历两个阶段:

1)等待数据准备(Waiting for the data to be ready)

2)将数据从内核拷贝到进程(线程所在的)的内存中(Copying the data from the kernel to the process or thread)

四个概念

同步 synchronous

一个进程或线程在提交任务或发出调用后,要等待返回的最终结果后才算执行完毕,然后才会继续执行下一步操作

所谓同步,就是没得到最终结果之前,就不会继续下一步操作。
这就好比单行道过收费站一样,一个接着一个,只有上一个结束收费下一辆车才能开始进入站窗口交费。每辆车就相当与一个进程或线程。
其实绝大多数的函数都是同步调用,我们都是拿到结果之后在进行下一步代码操作。

而我们在谈及同步和异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。

异步 asynchronous

异步的概念与同步相对,当一个进程或线程发起一个异步功能调用或者说提交任务方式是异步提交的时,不会立即得到结果。它会继续执行下一步代码。而异步功能完成后,通过状态、通知或回调函数来通知调用者。

如果是通过状态在通知,那么调用者需要每隔一段时间检查一次结果是否返回,效率很低。

如果使用通知或者回调的方式,则进程或线程就不需要再做额外的操作,效率很高。

阻塞 blocking

首先阻塞是一种状态,一种进程或者线程的状态,不要把阻塞与同步混淆。

也就是说当进程或线程发起调用或提交任务之后,结果返回之前,比如遇到IO操作,则调用者(该进程或线程)就会被挂起,然后只有等到有结果后才会再将阻塞的线程激活。

非阻塞 non-blocking

非阻塞与阻塞相反,就算不能立即得到结果也不会将其挂起,而是会继续执行下一步。

四种常见IO模型 

 IO模型主要分五种

    blocking IO    阻塞IO
    nonblocking IO  非阻塞IO
    IO multiplexing  复用型IO
    signal driven IO  信号驱动型IO
    asynchronous IO  异步IO

他们直接的区别点在于过程、进程(或线程)的状态、发起调用方式

首先最常见的IO模型:blocking IO

blocking IO

默认情况下,所有的socket都是阻塞IO,一个典型的read操作流程

non-blocking IO

设置socket可以使其变为非阻塞,当对一个非阻塞的socket执行读操作的时候,流程会不一样:

从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,
而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。
用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,
并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。 所以,用户进程其实是需要不断的主动询问kernel数据好了没有。

IO multiplexing

IO multiplexing这个词可能有点陌生,但是如果我说select,epoll,大概就都能明白了。有些地方也称这种IO方式为event driven IO。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(多说一句。所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

Asynchronous I/O

 

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

小结

 blocking和non-blocking的区别

调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在内核准备数据这个阶段,进程是不会非阻塞状态。

synchronous IO和asynchronous IO的区别

同步IO在进行IO操作的时候会将进程阻塞,而异步IO却不会。

所以blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO,而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

 最后各个IO Model的比较如图所示:

 

 

 

 

 

 

 

 

———————————————— 
版权声明:本文为CSDN博主「historyasamirror」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/historyasamirror/article/details/5778378

posted @ 2019-08-21 20:23  MrSu  阅读(470)  评论(1编辑  收藏  举报