11 python 协程和异步io
并发、并行、同步、异步、阻塞、非阻塞
并发:是指一个时间段内,有几个程序在同一个cpu上运行,但是任意时刻只有一个cpu上运行
并行:是指任意时刻点上,有多个程序同时运行在多个cpu上
同步:是指代码调用IO操作时,必须等待IO操作完成才返回的调用方式
异步:是指代码调用IO操作时,不必等待IO操作完成就返回的调用方式
阻塞:是指调用函数时候当前线程被挂起
非阻塞:是指调用函数时候当前线程不会被挂起,而是立即返回
IO 多路复用 (select、poll 和 epoll)
C10K问题
如何在1Gcpu,2G内存,1g网络环境下,让单台服务器同时为1万个客户端提供FTP服务
Unix下五中I/O模型
阻塞式I/O
非阻塞式I/O
I/O多路复用
信号驱动I/O
异步I/O
阻塞式I/O
在等待数据返回的时候存在大量的时间等待过程
非阻塞式I/O
虽然在遇到I/O操作是不在阻塞,但是需要CPU不断的去询问是否已成功准备好,消耗大量CPU操作,在数据准备好后,把数据从内核赋值到用户空间还是要阻塞
I/O多路复用
虽然在遇到I/O操作时,他也是处于阻塞的状态,但是它和阻塞I/O的区别是,它可以监听多个socket,如果有一个socket的状态发生变化会立即返回处理,即把要监听的多个socket放到一个列表中监听
主要有3种模型:select poll epoll
epoll并不代表一定比select好
在并发高的情况下,连接活跃度不是很高, epoll比select
并发性不高,同时连接很活跃, select比epoll好
异步IO
遇到IO操作时立即返回,等待数据从内核赋值到用户内存空间后,在通知程序来执行,无IO等待时间
select+回调+事件循环获取html
非阻塞io虽然在遇到io操作时不会等待,但会消耗大量的cpu进行询问,在数据从内核赋值到用户空间还是有一定的等待时间。
和同步的等待时间没什么区别,但是它可以在阻塞的时候除了不停的询问外,还可以在别的事情
#1. epoll并不代表一定比select好 # 在并发高的情况下,连接活跃度不是很高, epoll比select # 并发性不高,同时连接很活跃, select比epoll好 #通过非阻塞io实现http请求 import socket from urllib.parse import urlparse #使用非阻塞io完成http请求 def get_url(url): #通过socket请求html url = urlparse(url) host = url.netloc path = url.path if path == "": path = "/" #建立socket连接 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.setblocking(False) try: client.connect((host, 80)) #阻塞不会消耗cpu except BlockingIOError as e: pass #不停的询问连接是否建立好, 需要while循环不停的去检查状态 #做计算任务或者再次发起其他的连接请求 while True: try: client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8")) break except OSError as e: pass data = b"" while True: try: d = client.recv(1024) except BlockingIOError as e: continue if d: data += d else: break data = data.decode("utf8") html_data = data.split("\r\n\r\n")[1] print(html_data) client.close() if __name__ == "__main__": get_url("http://www.baidu.com")
同步http请求和select+回调+时间循环请求对比
同步http请求20个url
import socket from urllib.parse import urlparse def get_url(url): #通过socket请求html url = urlparse(url) host = url.netloc path = url.path if path == "": path = "/" #建立socket连接 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((host, 80)) #阻塞不会消耗cpu client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8")) data = b"" while True: d = client.recv(1024) if d: data += d else: break data = data.decode("utf8") html_data = data.split("\r\n\r\n")[1] print(html_data) client.close() if __name__ == "__main__": import time start_time = time.time() for url in range(20): url = "http://shop.projectsedu.com/goods/{}/".format(url) get_url(url) print(time.time()-start_time)
执行时间如下
select+回调+时间循环请求20个url
回调+事件循环+select(poll\epoll) 是目前各大框架底层常用的IO多路复用技术
1 事件循环,不停的请求socket的状态并调用对应的回调函数,通过loop事件循环 不断循环检测ready是否有返回值 ready = selector.select() ready有返回值的话执行对应的回调函数
2. select本身是不支持register模式, selectors在select进行了封装可以把socket注册到select中
selector.register("socket文件类型", 事件类型(读/写), socket建立成功后的回调函数地址)
3. socket状态变化以后的回调是由程序员完成的
import socket from urllib.parse import urlparse from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE selector = DefaultSelector() #使用select完成http请求 urls = [] stop = False class Fetcher: def connected(self, key): selector.unregister(key.fd) self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode("utf8")) selector.register(self.client.fileno(), EVENT_READ, self.readable) def readable(self, key): d = self.client.recv(1024) if d: self.data += d else: selector.unregister(key.fd) data = self.data.decode("utf8") html_data = data.split("\r\n\r\n")[1] print(html_data) self.client.close() urls.remove(self.spider_url) if not urls: global stop stop = True def get_url(self, url): self.spider_url = url url = urlparse(url) self.host = url.netloc self.path = url.path self.data = b"" if self.path == "": self.path = "/" # 建立socket连接 self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.client.setblocking(False) try: self.client.connect((self.host, 80)) # 阻塞不会消耗cpu except BlockingIOError as e: pass #注册 selector.register(self.client.fileno(), EVENT_WRITE, self.connected) def loop(): #事件循环,不停的请求socket的状态并调用对应的回调函数 while not stop: ready = selector.select() for key, mask in ready: call_back = key.data call_back(key) if __name__ == "__main__": fetcher = Fetcher() import time start_time = time.time() for url in range(20): url = "http://shop.projectsedu.com/goods/{}/".format(url) urls.append(url) fetcher = Fetcher() fetcher.get_url(url) loop() print(time.time()-start_time)
执行时间如下
协程是什么
回调虽然有很到的性能,但是对于我们而言也面临了不少的问题
1 可读性差
2 共享状态管理困难
3 异常处理困难
我们即想使用回调的高性能,又想使用起来简单,所以就有了协程
协成类似于一个可以暂停的函数(可以向暂停的地方传入值),可以在一个线程中进行来回切换
生成器进阶-send、close和throw方法
send 传值给生成器 yield产出值
def gen_func(): #1. 可以产出值, 2. 可以接收值(调用方传递进来的值) html = yield "http://projectsedu.com" print(html) return "bobby" if __name__ == "__main__": gen = gen_func() #在调用send发送非none值之前,我们必须启动一次生成器, 方式有两种1. gen.send(None), 2. next(gen) url = gen.send(None) #download url html = "bobby" print(gen.send(html)) #send方法可以传递值进入生成器内部,同时还可以重启生成器执行到下一个yield位置 print(gen.send(html)) # 抛出异常 StopIteration
输出结果如下
close关闭生成器
def gen_func(): #1. 可以产出值, 2. 可以接收值(调用方传递进来的值) try: yield "http://projectsedu.com" except RuntimeError: pass yield 2 yield 3 return "bobby" if __name__ == "__main__": gen = gen_func() print(next(gen)) gen.close() # 关闭生成器 print("bobby") #GeneratorExit是继承自BaseException, Exception
输出结果如下
throw 向生成器内部抛出一个异常
def gen_func(): yield "http://projectsedu.com" yield 2 yield 3 return "bobby" if __name__ == "__main__": gen = gen_func() print(next(gen)) gen.throw(Exception, "download error")
输出结果如下
yield 和 yield from 区别
yield 返回的是一个具体的值
from itertools import chain my_list = [1,2,3] def my_chain(*args, **kwargs): print(args) for my_iterable in args: for value in my_iterable: yield value for value in my_chain(my_list, range(5,10)): print(value)
输出结果如下
yield from 返回的是一个可迭代的对象,在内部帮助我们遍历取值
my_list = [1,2,3] def my_chain(*args, **kwargs): print(args) for my_iterable in args: yield from my_iterable for value in my_chain(my_list, range(5,10)): print(value)
输出结果如下
生成器进阶-yield from
yield from 在内部帮助我们做了大量的异常处理
一段伪代码如下
def g1(gen): yield from gen def main(): g = g1() g.send(None)
1 main 调用方 g1 (委托生成器) gen 子生成器
2 yield from 会在调用方 main 与子生成器 gen 之间建立一个双向通道
3 在这个双向通道内 调用方main可以直接传值给子生成器gen
4 yield from 会把子生成器的最终的值返回给委托生成器
案例:统计一个字典中value为列表对其求和返回
main调用方调用委托生成器middle 通过yield from 和子生成器 sales_sum 建立双向通道
通过双向通道向其传值,通过send(none) 编程停止,子生成器通过yield from 把最终的统计结果返回给委托生成器
#python3.3新加了yield from语法 final_result = {} def sales_sum(pro_name): total = 0 nums = [] while True: x = yield print(pro_name+"销量: ", x) if not x: break total += x nums.append(x) return total, nums def middle(key): while True: final_result[key] = yield from sales_sum(key) print(key+"销量统计完成!!.") def main(): data_sets = { "bobby牌面膜": [1200, 1500, 3000], "bobby牌手机": [28,55,98,108 ], "bobby牌大衣": [280,560,778,70], } for key, data_set in data_sets.items(): print("start key:", key) m = middle(key) # 委托生成器 m.send(None) # 预激middle协程 for value in data_set: m.send(value) # 给协程传递每一组的值 m.send(None) print("final_result:", final_result) if __name__ == '__main__': main()
输出结果如下
async和await
python为了将语义变得更加明确,因为有时候我们通过yield 关键字很难区分一个函数是生成器还是协程,就引入了async和await关键词用于定义原生的协程
import types @types.coroutine def downloader(url): yield "zhangbiao" async def download_url(url): html = await downloader(url) return html if __name__ == "__main__": coro = download_url("http://www.imooc.com") res = coro.send(None) print(res)
输出结果如下