#1. 同步与异步针对的是函数/任务的调用方式:同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活状态。而异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行当,函数返回的时候通过状态、通知、事件等方式通知进程任务完成。#2. 阻塞与非阻塞针对的是进程或线程:阻塞是当请求不能满足的时候就将进程挂起,而非阻塞则不会阻塞当前进程
同步
#所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。按照这个定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。#举例:#1. multiprocessing.Pool下的apply #发起同步调用后,就在原地等着任务结束,根本不考虑任务是在计算还是在io阻塞,总之就是一股脑地等任务结束#2. concurrent.futures.ProcessPoolExecutor().submit(func,).result()#3. concurrent.futures.ThreadPoolExecutor().submit(func,).result()
异步
#异步的概念和同步相对。当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态、通知或回调来通知调用者。如果异步功能用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一 种很严重的错误)。如果是使用通知的方式,效率则很高,因为异步功能几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。#举例:#1. multiprocessing.Pool().apply_async() #发起异步调用后,并不会等待任务结束才返回,相反,会立即获取一个临时结果(并不是最终的结果,可能是封装好的一个对象)。#2. concurrent.futures.ProcessPoolExecutor(3).submit(func,)#3. concurrent.futures.ThreadPoolExecutor(3).submit(func,)
阻塞
#阻塞调用是指调用结果返回之前,当前线程会被挂起(如遇到io操作)。函数只有在得到结果之后才会将阻塞的线程激活。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。#举例:#1. 同步调用:apply一个累计1亿次的任务,该调用会一直等待,直到任务返回结果为止,但并未阻塞住(即便是被抢走cpu的执行权限,那也是处于就绪态);#2. 阻塞调用:当socket工作在阻塞模式的时候,如果没有数据的情况下调用recv函数,则当前线程就会被挂起,直到有数据为止。
非阻塞
#非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程。
一、IO发生时涉及的对象和步骤
对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,该操作会经历两个阶段:
#1)等待数据准备 (Waiting for the data to be ready)#2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
五种IO Model:
阻塞IO (blocking IO)
非阻塞IO (nonblocking IO)
IO多路复用 ( IO multiplexing)
异步IO (signal driven IO)
信号驱动(了解) (asynchronous IO)
阻塞IO
socket模块默认就是阻塞的问题:同一时间只能服务一个客户端方法1:多线程 优点:如果并发量不高 效率是较高的 因为每一个客户端都有单独线程来处理 弊端:不可能无限的开启线程 线程也需要占用资源方式2:多进程 优点: 可以多个CPU并行处理 弊端: 占用资源非常大,一旦客户端稍微多一点 立马就变慢了线程池: 优点: 保证了服务器正常稳定运行,还帮你负责创建和销毁线程,以及任务分配 弊端: 一旦并发量超出最大线程数量,就只能等前面的运行完毕进程池:真正导致效率低的是阻塞问题 但是上述几个方法 并没有真正解决阻塞问题 仅仅是避开了阻塞问题
非阻塞IO
setblocking(False)
在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有
import socketimport times = socket.socket()s.bind(("127.0.0.1",9999))s.listen()# 设置socket 是否阻塞 默认为Trues.setblocking(False)# 所有的客户端socketcs = []# 所有需要返回数据的客户端send_cs = []while True: # time.sleep(0.2) try: c,addr = s.accept() # 三次握手 print("run accept") cs.append(c) #存储已经连接成功的客户端 except BlockingIOError: # 没有数据准备 可以作别的事情 # print("收数据") for c in cs[:]: try: data = c.recv(1024) if not data: c.close() cs.remove(c) print(data.decode("utf-8")) # 把数据和连接放进去 send_cs.append((c, data)) #c.send(data.upper()) # io # send也是io操作 在一些极端情况下 例如系统缓存满了 放不进去 那肯定抛出 # 非阻塞异常 这时候必须把发送数据 单独拿出来处理 因为recv和send都有可能抛出相同异常 # 就无法判断如何处理 except BlockingIOError: continue except ConnectionResetError: c.close() # 从所有客户端列表中删除这个连接 cs.remove(c) # print("发数据") for item in send_cs[:]: c,data = item try: c.send(data.upper()) # 如果发送成功就把数据从列表中删除 send_cs.remove(item) except BlockingIOError: # 如果缓冲区慢了 那就下次再发 continue except ConnectionResetError: c.close() # 关闭连接 send_cs.remove(item) # 删除数据 # 从所有客户端中删除这个已经断开的连接 cs.remove(c)
补充:
li = [1,2,3,4,5,6]for i in li[:]: # li.remove(i)print(li)
但是非阻塞IO模型绝不被推荐
其优点:#能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在“”同时“”执行)其缺点#1. 无限循环调用recv(),CPU占用率太高;#2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
多路复用IO
IO多路复用 用一个线程来并发处理所有的客户端 原本我们是直接向操作系统 要数据, 如果是阻塞IO 没有数据就进入阻塞状态 非阻塞IO 没有数据就抛出异常 然后继续询问操作系统 在多路复用模型中,筛选出已经准备就绪的socket,会将可读和科协的分别放到不同的列表中 既然是已经就绪 那么执行recv或是send 就不会在阻塞 select模块只有一个函数就是select 参数1:r_list 需要被select检测是否是可读的客户端 把所有socket放到该列表中,select会负责从中找出可以读取数据的socket 参数2:w_lirt 需要被select检测是否是可写的客户端 把所有socket放到该列表中,select会负责从中找出可以写入数据的socket 参数3:x_list 存储要检测异常条件 ....忽略即可 返回一个元组 包含三个列表 readables 已经处于可读状态的socket 即数据已经到达缓冲区 writeables 已经处于可写状态的socket 即缓冲区没满 可以发送... x_list:忽略 从可读或写列表中拿出所有的socket 依次处理它们即可
import socketimport timeimport selects = socket.socket()s.bind(("127.0.0.1",9999))s.listen()# 在多路复用中 一旦select交给你一个socket 一定意味着 该socket已经准备就绪 可读或是可写# s.setblocking(False)r_list = [s]w_list = []# 存储需要发送的数据 已及对应的socket 把socket作为key 数据作为valuedata_dic = {}while True: readables,writeables,_ = select.select(r_list,w_list,[]) # 接收数据 以及服务器建立连接 for i in readables: if i == s:# 如果是服务器 就执行accept c,_ = i.accept() r_list.append(c) else: # 是一个客户端端 那就recv收数据 try: data = i.recv(1024) if not data: #linux 对方强行下线或是 windows正常下线 i.close() r_list.remove(i) continue print(data) # 发送数据 不清楚 目前是不是可以发 所以交给select来检测 w_list.append(i) data_dic[i] = data # 把要发送的数据先存在 等select告诉你这个连接可以发送时再发送 except ConnectionResetError:# windows强行下线 i.close() r_list.remove(i) # 从检测列表中删除 # 发送数据 for i in writeables: try: i.send(data_dic[i].upper()) # 返回数据 #data_dic.pop(i) #w_list.remove(i) except ConnectionResetError: i.close() finally: data_dic.pop(i) # 删除已经发送成功的数 w_list.remove(i) # 从检测列表中删除这个连接 如果不删除 将一直处于可写状态
select监听fd变化的过程分析:
#用户进程创建socket对象,拷贝监听的fd到内核空间,每一个fd会对应一张系统文件表,内核空间的fd响应到数据后,就会发送信号给用户进程数据已到;#用户进程再发送系统调用,比如(accept)将内核空间的数据copy到用户空间,同时作为接受数据端内核空间的数据清除,这样重新监听时fd再有新的数据又可以响应到了(发送端因为基于TCP协议所以需要收到应答后才会清除)。
该模型的优点:
#相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。
该模型的缺点:
#首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异,所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。#其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。