11.python并发入门(part14阻塞I/O与非阻塞I/O,以及引入I/O多路复用)

一、初步了解什么是I/O模型。

1.回顾,用户态与内核态。

操作系统位于应用程序和硬件之间,本质上是一个软件,它由内核以及系统调用组成。

内核:用于运行于内核态,主要作用是管理硬件资源。

系统调用:运行与用户态,为应用程序提供系统调用的接口。

操作系统的核心,就是内核,内核具有访问底层硬件设备的权限,为了保证用户无法直接对内核进行操作,并且保证内核的安全,所以就划分了用户空间和内核空间。

2.回顾进程切换。

如果说要实现进程之间的切换,那么进程需要有能力挂起,有能力回复,这样才叫切换。

进程与进程之间的切换是由操作系统来完成的!

假如说有多个进程,其中有个进程正在cpu上运行,现在需要切换到另一个线程,之间都发生了什么?

第一步:保存cpu上下文,程序计数器,以及寄存器。

第二步:更新PCB信息,把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列,选择另一个进程执行,并更新其PCB。

第三步:更新内存管理的数据结构。

第四步:恢复处理器的上下文

#这几步理解不了无所谓....其实主要就是想说明,进程和进程之间进行切换真的很耗资源!!!

3.回顾进程阻塞。

正在执行的进程,这个进程在等待数据,或者说,请求资源失败,或者,等待的某种事物没有发生。

系统就会将这个进程的状态改为阻塞(block)状态。

阻塞,也可以说是进程自己的一种主动行为。

只有进程处于运行状态的时候,才会获得cpu资源,如果当一个进程变为了阻塞状态,是不会对cpu资源造成占用的。(阻塞完全不占用cpu资源。)

4.回顾文件描述符。

个人觉得文件描述符这种东西,只有在unix,linux(各种类unix系统)上才会存在。

是一个用来引用文件的一个很抽象的概念。

文件描述符的表达形式,是一个非负数的整数。

我一直把文件描述符理解为一个索引,这个索引指向了每个进程打开的每个文件的一个记录表。

当一个应用程序或者一个进程,打开或者创建一个文件的时候,内核就会返回一个文件描述符。

linux的文件描述符一旦被耗尽,会导致一些应用程序无法正常运行,无法创建socket文件等....

5.缓存I/O。(数据在内核态和用户态之间来回复制)

拿socket通信来举例吧,两台机器之间通过socket来进行通信,当计算机A给计算机B发送一个数据的时候,是直接发送给计算机B的吗?其实并不是,计算机A在发送数据时,首先会从用户态转换为内核态,然后把数据交给自己的网卡,再对外进行发送。

接收端在接收数据的时候,由内核态转交给用户态。

数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。用户空间没法直接访问内核空间的。(可以理解为数据从内核态被复制到了用户态)

通过上面的例子,说明了缓存I/O是由缺点的,数据在传输的过程中,数据需要在应用程序地址空间(用户态),和内核空间(内核态)中进行来回的数据拷贝操作!对CPU和内存的开销都是非常大的!

6.I/O模型简单介绍。

I/O模型到底是什么?不同的I/O模型到底有什么区别?

就拿socket网路通信来举例吧,socket通信属于网络I/O,执行accept(等待连接)和recv(等待数据接收)时,会涉及到两个系统对象,一个是调用这个I/O操作的线程或者进程,另外就是系统内核。

  1. 等待数据的准备。
  2. 然后将数据从内核空间拷贝到进程中

下面总结了四种比较常用的I/O模型:

(1).blocking IO(阻塞IO)(unix/linux默认的一中I/O模型)

在linux和类unix系统下,所有的socket对象都是blocking I/O模型,下面是一个阻塞I/O模型的运行流程。

假设现在有两台主机,主机A和主机B要通过socket去通信,主机A现在执行了一个accept()要去等待主机B来连接,这个时候程序中的accepet(这个过程是在向操作系统索要数据!),就会发起一个系统调用“recvfrom”给内核,在主机A执行accept的时候,主机B有去连接主机A吗?主机B什么时候会去连接主机A?这些是未知的,这个时候,就出现了一个等待数据的现象,这个等待现象是操作系统的内核造成的,内核会去等待数据,这个等待的过程就是阻塞的!!主机A的代码就会一直卡在accept的位置,无法继续执行后面的内容(一直等在这)。

这个时候,主机B执行了connect,连接到了主机A,此时内核拿到了数据!(需要注意!此时拿到数据的是内核!)程序无法直接拿到放在内核空间的数据,所以接下来内核会把数据拷贝一份给用户地址空间。此时此刻!!我们的程序才会真正拿到这个数据!

这就是阻塞IO的一个运行流程。

如果说的再简单点儿:

对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

(整个过程中只发起了一次系统调用)

(阻塞IO是同步的,)

(这种I/O模型效率偏低,但是数据可以得到及时的处理。)

(2).nonbloking IO (非阻塞IO)

其他的系统不太清楚,但是在linux系统下,可以通过设置socket(setblocking方法),可以改变socket的阻塞模型为非阻塞模型,非阻塞模型的运行流程如下:

还拿前面说的主机A和主机B通信的例子来说,主机A和主机B要通过socket去通信,主机A现在执行了一个accept()要去等待主机B来连接,这时会发起一个recvfrom的系统调用,此时如果kernel中还没有收到主机B的连接,或者说数据还没有准备好,这时不会去阻塞这个进程,而是返回一个error(IO阻塞异常)(主机A这端的程序不会一直等待,而是会直接返回结果)。此时用户的程序去做一个判断,判断这次recvfrom系统调用的结果是不是error,如果是error(I/O阻塞异常)则说明了数据还没有准备好,但是这个程序不会被阻塞,还可以去做点其他事情,也可以再次accept或者recv(发起recvfrom系统调用),一旦内核中的数据准备好了,并且再次收到了程序的系统调用,那么,马上就会把数据从内核的内存空间直接拷贝到用户的内存空间。

所以,使用非阻塞IO模型的情况下,用户的进程需要不停的询问内核,是否有把数据准备好。

在网络I/O中,使用非阻塞I/O模型,发起recvfrom这个系统调用后,进程没有被阻塞,内核直接返回结果给进程,如果数据没准备好会返回一个错误,进程在返回之后,可以干点别的事情,然后再发起recvform系统调用,一直重复上面的过程。

循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。

最后还有一点要注意!!!!非阻塞I/O模型,并不是全程无阻塞的,内核要给用户控件拷贝数据的时候,这个时候时间虽然非常短!!但这个过程一定是阻塞的!!!!

下面是个非阻塞模型的网络I/O示例:

服务端:

import time

import socket

sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

sk.setsockopt

sk.bind((‘127.0.0.1‘,6667))

sk.listen(5)

sk.setblocking(False)  #设置非阻塞I/O模型

while True:

try:

print (‘waiting client connection .......‘)

connection,address = sk.accept()   # 进程主动轮询

print("+++",address)

client_messge = connection.recv(1024)

print(str(client_messge,‘utf8‘))

connection.close()

except Exception as e:

print (e)

time.sleep(4)

客户端:

import time

import socket

sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

while True:

sk.connect((‘127.0.0.1‘,6667))

print("hello")

sk.sendall(bytes("hello","utf8"))

time.sleep(2)

break

其实这种非阻塞I/O模型看起来效率很高,但是也有缺点:

首先,这种非阻塞的I/O模型会发送很多的系统调用。

其次,数据无法及时的被处理,先来看看上面那个例子,每次轮询内核数据的时候,如果发现内核没有准备好数据,需要sleep 4秒,在sleep的这4秒钟,刚sleep两秒,内核中的数据准备好了,但是这时sleep才刚执行了2秒,还有2秒没有sleep完,需要再等待两秒,才会从新去轮询检查一次内核的数据(也就是说,想拿到数据,还要再过两秒),所以说,数据没有办法及时得到处理。

(3).IO multiplexing(I/O多路复用)

I/O多路复用的好处就在于单个进程,可以同时处理多个网络连接的I/O,其实就是又select/epoll这两个函数去不断的轮询所有的socket,一旦有socket的状态发生变化,就会去通知用户进程(程序)。

I/O多路复用的基本运行流程图如下:

首先由select函数发起一个系统调用,这个系统调用也叫select,一但调用了这个select,整个进程就会进入阻塞状态,此时,内核回去检测,所有select负责的socket,当select负责的这些socket中,其中只要有一个socket的状态发生了改变,那么select就会立刻返回。用户就可以收到来自这个socket发来的数据。

其实select这个图和阻塞I/O很像,貌似没什么区别,但是,使用select是由优势的,select可以同时处理多个连接。

如果处理的并发连接数不是很高的情况下,使用这种IO多路复用,可能还没有多线程+IO阻塞的性能好,甚至可能会加大延迟。

注意!!IO多路复用select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

在I/O多路复用的模式中,每一个socket,一般都设置为非阻塞,但是整个用户的进程其实是一直被阻塞的!和默认的I/O阻塞不同,这个阻塞是因为进程被select函Lock导致的,和I/O造成的阻塞其实不是一回事。

一旦select函数返回的结果中有内容可以拿了,那么进程就可以调用accpet或者recv将位于内核空间的数据copy到用户空间中。

只有在处理特别多的连接数时,才需要考虑使用select函数,如果只处理单个连接,无法体现出select的优势!

下面是select函数基本用法的一个示例:

服务端:

#!/usr/local/bin/python2.7

# -*- coding:utf-8 -*-

import socket

import select

socket_obj = socket.socket()

socket_obj.bind(("127.0.0.1",8889))

socket_obj.listen(5)

while True:

r,w,e = select.select([socket_obj,],[],[],5) #用来检测socket_obj这个socket对象是否产生了变化,如果有变化,返回给变量r,(5代表5秒后,即使socket没有任何变化,代码也会向下执行!)

#一旦这个socket产生了变化之后,变量r就有内容了。

for i in r:

print "conn ok!"

print (‘wait.....‘)

客户端:

#!/usr/local/bin/python2.7

# -*- coding:utf-8 -*-

import socket

socket_client = socket.socket()

socket_client.connect(("127.0.0.1",8889))

while True:

inpt = raw_input(">>>").strip()

socket_client.send(inpt.encode("utf-8"))

data = socket_client.recv(1024)

print data.decode("utf8")

(也许有人很好奇,select检测socket对象的机制是什么样的,其实select使用的是一种名为“水平触发”的检测机制。关于边缘触发和水平触发,后面会有详细介绍。)

(4).asynchronous IO (异步I/O)

也是一个用的不太多的I/O模型,下面是运行流程图。

用户程序首先发起一个aio_read系统调用,发起系统调用后,用户程序就会继续做其他的事情,当内核接收到了aio_read这个系统调用后,会像非阻塞IO一样,立刻返回一个结果,此时内核会等待数据准备完成, 然后将数据拷贝到内存空间,拷贝结束后,内核会给用户进程发送一个信号,告诉用户的进程,数据拷贝结束。

注意!!!

说道这里,有人可能会把异步I/O模型和非阻塞I/O模型的概念混淆,其实这两个I/O模型的区别还是很大的。

在使用非阻塞I/O模型的时候,进程的大部分时间都不会被Block,但是进程要主动去检查内核中的数据是否准备好,如果数据准备好了,还需要进程去发起一个recvfrom来将数据从内核空间拷贝到用户空间。

异步I/O模型则完全不同,感觉更像是用户进程把I/O操作直接交给了内核,等内核做完I/O操作,再去通知用户进程,在这个过程中,用户不需要去检查内核是否有将数据准备好,并且也不需要主动去将内核空间的数据拷贝到用户空间。

时间: 2024-11-18 01:53:32

11.python并发入门(part14阻塞I/O与非阻塞I/O,以及引入I/O多路复用)的相关文章

11.python并发入门(part12 初识协程)

一.协程的简介. 协程,又被称为微线程,虽然是单进程,单线程,但是在某种情况下,在python中的协程执行效率会优于多线程. 这是因为协程之间的切换和线程的切换是完全不一样的!协程的切换是由程序自身控制的(程序的开发者使用yield去进行控制,协程和协程之间的切换是可控制的,想什么时候切换就什么时候切换). 当使用多线程时,开的线程越多,协程的优势就越明显. 协程的另一个优点,就是无需锁机制,因为协程只有一个进程,和线程,不存在多线程或者多进程之间访问公共资源的冲突,所以说,在协程中无需加锁,如

11.python并发入门(part15 关于I/O多路复用)

一.为什么要产生I/O多路复用? 两个主机之间通信,主机A和主机B都需要开启socket,主机A首先要等待客户端来进行连接,这是会发起一个recvfrom的系统调用,如果主机B一直没有去连接主机A,没有给主机A发送任何数据,进程就会被阻塞,无法去做其他的事情(默认的阻塞I/O模型),一直阻塞到主机B去连接主机A,主机A收到了这个连接. 其实这种模式在单服务器,单客户端(两台主机之间单独通信)没有什么问题,如果说是多并发场景呢?服务端阻塞与第一个客户端发来的socket对象中,另外一个客户端2要发

11.python并发入门(part13 了解事件驱动模型))

一.事件驱动模型的引入. 在引入事件驱动模型之前,首先来回顾一下传统的流水线式编程. 开始--->代码块A--->代码块B--->代码块C--->代码块D--->......--->结束 每一个代码块里是完成各种各样事情的代码,但编程者知道代码块A,B,C,D...的执行顺序,唯一能够改变这个流程的是数据.输入不同的数据,根据条件语句判断,流程或许就改为A--->C--->E...--->结束.每一次程序运行顺序或许都不同,但它的控制流程是由输入数据和

11.python并发入门(part2 threading模块的基本使用)

一.在使用python多线程之前,你需要知道的. python的多线程中,实现并发是没有问题的,但是!!是无法实现真正的并行的. 这是因为python内部有个GIL锁(全局解释器锁),这个锁限制了在同一时刻,同一个进程中,只能有一个线程被运行!!! 二.threading模块的基本使用方法. 可以使用它来创建线程.有两种方式来创建线程. 1.通过继承Thread类,重写它的run方法. 2.创建一个threading.Thread对象,在它的初始化函数__init__中将可调用对象作为参数传入.

11.python并发入门(part7 线程队列)

一.为什么要用队列? 队列是一种数据结构,数据结构是一种存放数据的容器,和列表,元祖,字典一样,这些都属于数据结构. 队列可以做的事情,列表都可以做,但是为什么我们还要去使用队列呢? 这是因为在多线程的情况下,列表是一种不安全的数据结构. 为什么不安全?可以看下面这个例子: #开启两个线程,这两个线程并发从列表中移除一个元素. import threading import time l1 = [1,2,3,4,5] def pri(): while l1: a = l1[-1] print a

11.python并发入门(part9 多线程模块multiprocessing基本用法)

一.回顾多继承的概念. 由于GIL(全局解释器锁)的存在,在python中无法实现真正的多线程(一个进程里的多个线程无法在cpu上并行执行),如果想充分的利用cpu的资源,在python中需要使用进程. 二.multiprocessing模块的简介. multiprocessing是python中用来管理多进程的包,与threading用法非常类似,它主要使用multiprocessing.Process对象来创建一个进程对象,该进程可以运行在python的函数中. 该Process(进程)对象

11.python并发入门(part4 死锁与递归锁)

一.关于死锁. 死锁,就是当多个进程或者线程在执行的过程中,因争夺共享资源而造成的一种互相等待的现象,一旦产生了死锁,不加人工处理,程序会一直等待下去,这也被称为死锁进程. 下面是一个产生"死锁"现象的例子: import threading import time lock_a = threading.Lock() lock_b = threading.Lock() class test_thread(threading.Thread): def __init__(self): su

11.python并发入门(part11 进程同步锁,以及进程池,以及callback的概念)

一.关于进程锁. 其实关于进程锁没啥好讲的了,作用跟线程的互斥锁(又叫全局锁也叫同步锁)作用几乎是一样的. 都是用来给公共资源上锁,进行数据保护的. 当一个进程想去操作一个公共资源,它就可以给公共资源进程"上锁"的操作,其他进程如果也想去访问或者操作这个公共资源,那么其他的进程只能阻塞,等待刚刚的进程把锁释放,下一个进程才可以对这个公共资源进行操作. 下面是个关于进程锁的使用示范: #!/usr/local/bin/python2.7 # -*- coding:utf-8 -*- im

11.python并发入门(part10 多进程之间实现通信,以及进程之间的数据共享)

一.进程队列. 多个进程去操作一个队列中的数据,外观上看起来一个进程队列,只是一个队列而已,单实际上,你开了多少个进程,这些进程一旦去使用这个队列,那么这个队列就会被复制多少份. (队列=管道+锁) 这么做的主要原因就是,不同进程之间的数据是无法共享的. 下面是使用进程队列使多进程之间互相通信的示例: 下面这个例子,就是往进程队列里面put内容. #!/usr/local/bin/python2.7 # -*- coding:utf-8 -*- import multiprocessing de