前言
本章主要针对上一章节中的I/O模型做一些代码性的练习,尽管可能很少用到。但是你应当知道这些东西。还是具体结合上一篇的理论知识来看吧。最后将协程放在了扩展篇中,因为感觉目前能接触到的很多都是I/O多路复用+异步。协程属实不太常见,可能我层次低了。
阻塞I/O的socket服务端
阻塞点1:@H_403_17@accept()
等待三次握手建立链接
阻塞点2:@H_403_17@recv()
从内核态的缓冲区复制数据到用户态内存
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import socket server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) server.bind(("127.0.0.1",6666)) server.listen(5) while 1: conn,client_addr = server.accept() 阻塞点 1 : try: data = conn.recv(1024) 阻塞点 2 if not data: break conn.send(data.upper()) except ConnectionResetError as e: print(client_addr,关闭了双向链接") conn.close()
非阻塞I/O的socket服务端
阻塞点1:@H_403_17@recv()
从内核态的缓冲区复制数据到用户态内存
socket.SOCK_STREAM) server.setblocking(False) 设置为非阻塞 server.bind(()) server.listen(5: : conn,1)"> 不阻塞 except BlockingIOError as e: 捕捉异常 continue : data = conn.recv(1024) 读取数据时依然会阻塞 break 捕捉异常 continue conn.close()
select模块与I/O多路复用
阻塞点1:即描述符监听@H_403_17@r_list
,如果有事件触发就放到@H_403_17@r
列表中
阻塞点2:@H_403_17@recv()
从内核态的缓冲区复制数据到用户态内存
socket select server = socket.socket(family=socket.AF_INET,1)">) r_list = [server,] 被监听的socket对象,读取事件 w_list = [] 被监听的socket对象,写入事件 : r,w,e = select.select(r_list,w_list,[],0.5) """ r: 相当于一个空列表,当被监听的r_list中有任何对象能够读取数据了,则放到 r 列表中。 w: 同上,放的是可写的 e: 同上,放的是错误的 0.5: 循环监听的时间,每隔多少秒监听一次 """ for i in r: 当有任何可读就执行这里,这个就是事件循环。阻塞点 1 print(-----,r) if i == server: conn,addr = i.accept() r_list.append(conn) 将链接通道 conn 放入监听队列 else: 如果不是server端,那就是conn,链接通道 : data = i.recv(1024) 阻塞点2 not data: i.send(data.upper()) ConnectionResetError: 断开链接通道 将conn从可读列表中移除 1.开启服务端,监听可读事件列表 r_list,r_list = [socket对象,] 现在的r = [] 2.开启客户端,发送请求链接,r 列表新增了一个可读 r = [socket对象] ,执行for循环,条件成立 3.建立双向链接通道,将链接通道放入可读事件列表 r_list = [socket对象,conn1] # --- 客户端给服务端发消息 --- 1.此时的被监听列表 r_list = [socket对象,conn1] 2.客户端发了一个消息, r_list = [conn1,] 3. 执行for循环,条件不成立 # --- 客户端断开链接 --- 1.从监听列表 r_list 中删除了 conn1 """
selectors模块与异步
@H_403_17@
selectors
包可以自动根据操作系统选择@H_403_17@poll
还是@H_403_17@epoll
,windows不支持@H_403_17@epoll
。
现在很多框架的内部,如著名的异步框架@H_403_17@tornado
,@H_403_17@Twisted
,等等都是通过@H_403_17@epoll
实现的异步,其实@H_403_17@epoll
到底属于不属于异步在网络上有很大的争议,下面会有一节解释一下我认为比较好的一个答案。
如果是@H_403_17@epoll
,只有一个阻塞点,那就是@H_403_17@recv()
从内核态的缓冲区复制数据到用户态内存
selectors sel = selectors.DefaultSelector() 自动选择,select,poll,epoll def accept(sock,mask): conn,client_addr = sock.accept() 拿到双向链接 新的双向链接通道{0}来自{1}.format(conn,client_addr)) sel.register(conn,selectors.EVENT_READ,read) 为conn注册可读事件,绑定回调函数read read(conn,mask): try: <--针对 windows 平台客户端强制退出服务端会抛出异常 data = conn.recv(1024) not data: <-- 针对 UNIX 平台客户端强制退出服务端会无限收空 close_conn(conn,mask) conn.send(data.upper()) ConnectionResetError as e: close_conn(conn,mask) close_conn(conn,mask): sel.unregister(conn) 注销对conn双向链接通道的监听 socket.socket() server.bind(()) server.listen(5) server.setblocking(False) 设置非阻塞 sel.register(server,accept) 为server注册可读事件,回调函数为accept run(): while 1: 只要有注册的事件触发了,就会执行相应的回调函数,这里是死循环不断检测 events = sel.select(5) 最大监听时间 for key,mask in events: callback = key.data key.data 就是回调函数 callback(key.fileobj,mask) 执行回调,key.fileobj是监听的对象,也就是注册时的对象。 if __name__ == '__main__': run()
@H_403_17@其实我们这里已经实现了单线程来模拟多线程的监听,但是如果返回结果的时候是同时很多client端都来了,那么我们可以使用多线程来进行操作。
from concurrent.futures ThreadPoolExecutor events: with ThreadPoolExecutor(5) as pool: callback = key.data key.data 就是回调函数 pool.submit(callback,key.fileobj,1)"> 执行回调,key.fileobj是监听的对象,也就是注册时的对象。
@H_403_17@单线程中模拟多线程,是阻塞I/O以及单纯的非阻塞I/O都做不到的,他们都需要开多线程或者多进程,当然除了I/O多路复用,还有协程可以供我们使用。
扩展:epoll到底是属于同步还是异步
学习@H_403_17@
tornado
、@H_403_17@asyncio
这些异步网络库时,遇到了同样的问题,网上查到的也都说不明白,原来几个概念没搞清楚。1,I/O操作有多种,处理socket是一种,磁盘读写也是一种,暂时分为网络I/O和文件I/O、
2,I/O多路复用是操作系统级别的,属于linux操作系统的五种I/O模型中的一种,是操作系统级别同步非阻塞的。
3,异步网络库 twisted、tornado、asyncio所谓的异步,是应用级别的异步,底层确实是基于epoll实现,基本上都是处理网络 I/O,而且都是基于事件驱动的,使用时划分事件也大多是根据网络请求。
4,操作系统级别的异步I/O才是真正异步非阻塞的,然后并没有很多应用,貌似unix平台没有,windows NT平台有也很少,而且基本都是文件I/O。
5,I/O多路复用的实现用的比较多,linux平台的@H_403_17@
epoll
,windows平台的@H_403_17@select
等,基于@H_403_17@epoll
,@H_403_17@select
的应用大多是实现网络I/O。所以,遇到异步框架,异步网络库都应该知道是应用级别的异步,而且基本上都是基于@H_403_17@
epoll
/@H_403_17@select
实现的。已知@H_403_17@
tornado
会根据系统平台,选择@H_403_17@epoll
还是@H_403_17@select
。实现高并发有多种方式,python多进程可以利用多核优势,协程(@H_403_17@
gevent
、@H_403_17@asyncio
)可以实现应用级别的异步,@H_403_17@celery
实现任务异步,消息队列实现服务解耦等等,项目中可以根据实际情况选择或组合不同的方式。@H_403_17@
tornado
(web框架/异步网络库):进程+异步+@H_403_17@epoll
@H_403_17@
asyncio
:协程+@H_403_17@epoll
,使用中需要相应的异步库,常用@H_403_17@aiohttp
@H_403_17@
gevent
:@H_403_17@greenlet
+猴子补丁,猴子补丁把@H_403_17@socket
相关库改为非阻塞
摘自知乎:个人理解,应用层上的@H_403_17@epoll
确实是属于异步,而系统层上的@H_403_17@epoll
并非绝对意义上的异步。
扩展:协程
协程是不是一个真实存在的东西,但是线程和进程都是操作系统中真实存在的,协程也是做到在单线程中实现并发的效果。
协程是程序员创造出的一个不是真实的东西
协程可以认为是微线程,对一个线程进程进行分片,
使得线程在代码块之间可以进行来回切换,而不是在原来的基础上逐行执行。
一个线程中函数的切换,使得它的切换代价非常低。
注意:单纯的协程是没有什么用的,就只是做做切换,你甚至可以用生成器函数yield来完成。
@H_403_17@greenlet协程模块: 人为做切换
greenlet f1(): print(1) gr2.switch() 执行协程2 print(2) gr2.switch() f2(): print(3) gr1.switch() print(4 表示创建了2个协程。由1个线程创建出来的 gr1 = greenlet.greenlet(f1) gr2 = greenlet.greenlet(f2) gr1.switch() 执行协程1 ==== 执行结果 ==== 1 3 2 4 """
gevent模块 : @H_403_17@greenlet
+ IO切换,单纯的协程虽然没什么用,但是如果能够智能的碰见I/O就自己做切换,那就非常牛逼了。
gevent from gevent monkey monkey.patch_all() 对以下代码中有IO操作就切换 requests f(url): res = requests.get(url) 碰见 I/O 自动切换自另一个协程 print(url,res.text) gevent.joinall([ gevent.spawn(f,https://www.baidu.com/"), 协程 1 gevent.spawn(f,1)">https://www.yahoo.com/ 协程 2 ] )
1.协程能提高并发吗 ?
答:协程本身是无法提高并发的,但是协程+I/O切换可以.
2.单线程提高并发的方法是?
协程+IO切换 @H_403_17@gevent
,基于事件循环的异步非阻塞框架 @H_403_17@Twisted
。
3.进程,线程,协程的区别是什么?
进程是资源分配的最小单位,线程是cpu调度的最小单位.
在一个程序中可以有多个进行,一个进程最少有一个线程.
和其他语言相比较,其他语言几乎不用进程的,但是在Python中,它的进程和线程是有差异的,Python有个GIL锁,GIL锁保证一个进程在同一时刻只有一个线程被cpu调到.