Python高级编程和异步IO并发编程
一、协程和异步io
1、并发、并行、同步、异步、阻塞、非阻塞
- 并发: 是指一个时间段内,有几个程序在同一个cpu上运行,但是任意时刻只有一个程序在cpu上运行。
- 并行: 是指任意时刻点上,有多个程序同时运行在多个cpu上。
- 同步: 是指代码调用IO操作时,必须等待IO操作完成才返回的调用方式。
- 异步: 是指代码调用IO操作时,不必等待IO操作完成就返回的调用方式。
- 阻塞: 是指调用函数时候当前线程被挂起。
- 非阻塞: 是指调用函数时候当前线程不会被挂起,而是立即返回。
2、 IO 多路复用 (select、poll 和 epoll)
Unix下五种 I/O 模型:
- 阻塞式 I/O
- 非阻塞 I/O
- I/O 复用
- 信号驱动式 I/O
- 异步 I/O (POSIX的 aio_ 系列函数)
3、select+回调+事件循环获取html
** urllib.parse下的urlparse解析:
from urllib.parse import urlparse :用于解析url,将url分离开 ↓
# from urllib.parse import urlparse url = "http://www.baidu.com/test.py?a=hello&b=world " url = urlparse(url) # 结果: ParseResult(scheme='http', netloc='www.baidu.com', path='/test.py', params='', query='a=hello&b=world ', fragment='') # url.netloc = 'www.baidu.com' # url.path = '/test.py'
1)适用于linux epoll(未完善,问题:无法调用 EVENT_READ)
from urllib.parse import urlparse from selectors import DefaultSelector,EVENT_READ,EVENT_WRITE import socket selector = DefaultSelector() # 自动选择,win下选select ,linux下选epoll class Fetcher(object): # 模拟客户端(浏览器)向服务端请求url数据(即HTTP请求) # select + 回调函数 + 事件循环 ,经典模式 def readable(self, key): # 回调函数,接收服务器返回数据 print("1111111") d = self.client.recv(1024) # 不需要使用while True,只要有数据可读,自动会不断调用 if d: self.data += d else: selector.unregister(key.fd) # 注销掉该文件描述符(ready) data = self.data.decode("utf8") html_data = data.split("\r\n\r\n")[1] # html内容 print(html_data) self.client.close() def connected(self,key): # 回调函数,向服务器发送数据 selector.unregister(key.fd) # 注销掉该文件描述符(write/ready),此为write ,每次成功连接后都应该注销 (key:SelectorKey对象) # self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode("utf8"))# 发送请求头,编码成字节格式发送 self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\n\r\n".format(self.path, self.host).encode("utf8"))# 发送请求头,编码成字节格式发送 selector.register(self.client.fileno(), events=EVENT_READ, data=self.readable) # 注册 当可写时,说明服务器已经接收到我们的信息并给我们返回响应信息 def get_url(self,url): # url拆分 url = urlparse(url) # print(url) self.host = url.netloc self.path = url.path self.data = b"" if self.path == "": self.path = '/' # 建立连接 self.client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) self.client.setblocking(False) # 设为不阻塞 try: self.client.connect((self.host,80)) except BlockingIOError as e: pass # 注册 可写时说明已经连接成功,可以向服务器发送数据 selector.register(self.client.fileno(),EVENT_WRITE,self.connected) def loop(): # 事件循环,不停的请求socket的状态并调用对应的回调函数 # 1. select本身是不支持register模式 # 2. socket状态变化以后的回调是由程序员完成的 ready = selector.select() # 不断请求socket状态 for key , mask in ready: # 当状态是我们需要的,调用相应的回调函数。注意:回调函数实际是在这里调用的,而不是在注册时调用 call_back = key.data # 回调函数,key是SelectorKey对象,里面的data即是回调函数 call_back(key) # 执行回调函数 ,key:SelectorKey对象 if __name__ == '__main__': import time start_time = time.time() for i in range(6): # url = "http://101.200.45.75:9000/course/detail/{}/".format(i+1) url = "http://www.baidu.com" # url = "http://shop.projectsedu.com/goods/{}/".format(i) fetcher = Fetcher() fetcher.get_url(url) loop() # 类似select IO多路复用,不断查询socket状态 print(time.time() - start_time)
2)适用于win select
from urllib.parse import urlparse from selectors import DefaultSelector,EVENT_READ,EVENT_WRITE import socket selector = DefaultSelector() # 自动选择,win下选select ,linux下选epoll urls = [] # 用于判断爬虫是否全部爬完数据 stop = False class Fetcher(object): # 模拟客户端(浏览器)向服务端请求url数据(即HTTP请求) # select + 回调函数 + 事件循环 ,经典模式 def readable(self, key): # 回调函数,接收服务器返回数据 d = self.client.recv(1024) # 不需要使用while True,只要有数据可读,自动会不断调用 if d: self.data += d else: selector.unregister(key.fd) # 注销掉该文件描述符(ready) data = self.data.decode("utf8") html_data = data.split("\r\n\r\n")[1] # html内容 print(html_data) self.client.close() urls.remove(self.spider_url) if not urls: # 全部爬虫都完成数据爬取,准备关掉程序 global stop stop = True def connected(self,key): # 回调函数,向服务器发送数据 selector.unregister(key.fd) # 注销掉该文件描述符(write/ready),此为write ,每次成功连接后都应该注销 (key:SelectorKey对象) self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode("utf8"))# 发送请求头,编码成字节格式发送 # self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\n\r\n".format(self.path, self.host).encode("utf8"))# 发送请求头,编码成字节格式发送 selector.register(self.client.fileno(), events=EVENT_READ, data=self.readable) # 注册 当可写时,说明服务器已经接收到我们的信息并给我们返回响应信息 def get_url(self,url): self.spider_url = url # url拆分 url = urlparse(url) # print(url) self.host = url.netloc self.path = url.path self.data = b"" if self.path == "": self.path = '/' # 建立连接 self.client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) self.client.setblocking(False) # 设为不阻塞 try: self.client.connect((self.host,80)) except BlockingIOError as e: pass # 注册 可写时说明已经连接成功,可以向服务器发送数据 selector.register(self.client.fileno(),EVENT_WRITE,self.connected) def loop(): # 事件循环,不停的请求socket的状态并调用对应的回调函数 # 1. select本身是不支持register模式 # 2. socket状态变化以后的回调是由程序员完成的 while not stop: ready = selector.select() # 不断请求socket状态 for key , mask in ready: # 当状态是我们需要的,调用相应的回调函数。注意:回调函数实际是在这里调用的,而不是在注册时调用 call_back = key.data # 回调函数,key是SelectorKey对象,里面的data即是回调函数 call_back(key) # 执行回调函数 ,key:SelectorKey对象 if __name__ == '__main__': import time start_time = time.time() for i in range(6): # url = "http://101.200.45.75:9000/course/detail/{}/".format(i+1) url = "http://www.baidu.com" # url = "http://shop.projectsedu.com/goods/{}/".format(i) urls.append(url) # 用于判断爬虫是否爬取完毕 fetcher = Fetcher() fetcher.get_url(url) loop() # 类似select IO多路复用,不断查询socket状态 print(time.time() - start_time)
解析:
- selector = DefaultSelector():自动选择IO多路复用机制 , win下选择select ,linux下选择epoll
- selector.register(self.client.fileno(),EVENT_WRITE,self.connected):注册socket,当数据可写时调用,执行后面的回调函数(self.connected)
- selector.register(self.client.fileno(), events=EVENT_READ, data=self.readable):注册socket,当数据可读时调用,执行后面的回调函数(self.connected)
- selector.unregister(key.fd) # 注销掉该文件描述符(write/ready)
- loop() # 类似select IO多路复用,不断查询socket状态
使用上面的方法来实现IO多路复用,虽然比以往传统型同步操作 性能要高的多,但这种IO多路复用的方式也有缺点:
- 可读性差
- 共享状态管理困难
- 异常处理困难
4、协程是什么
-
C10M问题:
- 如何利用8核cpu、64G内存,在10gbps的网络上保持1000万并发连接。
-
高并发的问题:
- 1)回调模式编码复杂度高 # IO多路复用
- 2)同步编程的并发性不高 # 同步编码
- 3)多线程编程需要线程间同步,lock # 多线程编码
-
解决方法:
- 1)采用同步的方式去编写异步的代码
- 2)使用单线程去切换任务:
- 线程是由操作系统切换的,单线程切换意味着我们需要程序员自己去调度任务
- 不再需要锁,并发性高,如果单线程内切换函数,性能远高于线程切换,并发性更高
-
传统函数调用过程 A -> B -> C 。我们需要一个可以暂停的函数,并且可以在适当的时候恢复该函数的继续执行。此时出现了协程 -> 有多个入口的函数,可以暂停的函数(可以向暂停的地方传入值
协程概念理解:有多个入口的函数 , 或称 可以暂停的函数(同时可以向暂停的地方传入值)
5、生成器进阶- send、close和throw方法
1)send()方法
url = gen.send(None) :生成器首次调用时,如果使用send方法,参数必须为 None
.send():传递值到生成器内部,同时重启生成器执行到下一个yield位置
next(gen) : 一个个取值(生成器)
def gen_func(): #1. yield生成器 可以产出值, 2. 可以接收值(调用方传递进来的值) html = yield "http://projectsedu.com" print(html) # baby yield 2 return "hello" if __name__ == "__main__": gen = gen_func() # 生成生成器对象 #在调用send发送非none值之前,我们必须启动一次生成器, 方式有两种1. gen.send(None), 2. next(gen) url = gen.send(None) # url = next(gen) html = "baby" print(gen.send(html)) #send方法可以传递值进入生成器内部,同时还可以重启生成器执行到下一个yield位置 print(gen.send(html))
2) 生成器的close方法:
生成器对象的close方法会在生成器对象方法的挂起处抛出一个GeneratorExit异常。GeneratorExit异常产生后,系统会继续把生成器对象方法后续的代码执行完毕。参见下面的代码:
def myGenerator(): try: yield 1 print "Statement after yield" except GeneratorExit: print "Generator error caught" print "End of myGenerator" gen = myGenerator() print gen.next() gen.close() print "End of main caller"
# GeneratorExit继承于BaseException, 不是Exception
代码执行过程如下:
- 当调用gen.next方法时,会激活生成器,直至遇到生成器方法的yield语句,返回值1。同时,生成器方法的执行被挂起。
- 当调用gen,close方法时,恢复生成器方法的执行过程。系统在yield语句处抛出GeneratorExit异常,执行过程跳到except语句块。当except语句块处理完毕后,系统会继续往下执行,直至生成器方法执行结束。
执行结果:
1 1 2 Generator error caught 3 End of myGenerator 4 End of main caller
需要注意的是,GeneratorExit异常的产生意味着生成器对象的生命周期已经结束。因此,一旦产生了GeneratorExit异常,生成器方法后续执行的语句中,不能再有yield语句,否则会产生RuntimeError,同时 RuntimeError会向主方法抛出异常,后面的代码的将不会再被执行
3)生成器的throw方法:
def gen_func(): try: yield "http://projectsedu.com" except Exception as e: pass yield 2 yield 3 return "MJ" if __name__ == "__main__": gen = gen_func() print(next(gen)) #代码执行到第一个yield print(gen.throw(Exception, "download error")) # 代码执行到第二个yield ,手动抛异常 print(next(gen)) # 代码执行到第三个yield ,停止 gen.throw(Exception, "download error") # 再抛异常,没有捕捉
运行结果:
1 http://projectsedu.com 2 Traceback (most recent call last): 3 2 4 3 5 File "G:/Python/python_sample/test.py", line 15, in <module> 6 gen.throw(Exception, "download error") 7 File "G:/Python/python_sample/test.py", line 7, in gen_func 8 yield 3 9 Exception: download error
6、生成器进阶-yield from
yield from 与 yield 不同点:
yield:给到什么值,返回就是什么值,比如:yield range(10) , 返回结果就是:range(0,10)
yield from :会将给到的数据一个个返回,比如:yield from range(10) ,返回结果是:0 ,1 , 2 ,3 ,4 ,5, ,6 ,7 ,8 ,9 ,10
将有yield from的生成器当作委托生成器 ,yield from(委托生成器)会在调用方与子生成器之间建立一个双向通道,yield from作用解析:
假设 A 函数中有这样一个语句:
yield from B()
B() 返回的是一个可迭代(iterable)的对象b,那么A()会返回一个 generator——照我们的命名规范,名字叫a——那么:
b迭代产生的每个值都直接传递给a的调用者。
所有通过send方法发送到a的值都被直接传递给b. 如果发送的 值是None,则调用b的__next__()方法,否则调用b的send 方法。如果对b的方法调用产生StopIteration异常,a会继续 执行yield from后面的语句,而其他异常则会传播到a中,导 致a在执行yield from的时候抛出异常。
如果有除GeneratorExit以外的异常被throw到a中的话,该异常 会被直接throw到b中。如果b的throw方法抛出StopIteration, a会继续执行;其他异常则会导致a也抛出异常。
如果一个GeneratorExit异常被throw到a中,或者a的close 方法被调用了,并且b也有close方法的话,b的close方法也 会被调用。如果b的这个方法抛出了异常,则会导致a也抛出异常。 反之,如果b成功close掉了,a也会抛出异常,但是是特定的 GeneratorExit异常。
a中yield from表达式的求值结果是b迭代结束时抛出的 StopIteration异常的第一个参数。
b中的return 语句实际上会抛出StopIteration( ) 异常,所以b中return的值会成为a中yield from表达式的返回值
#pep380 #1. RESULT = yield from EXPR可以简化成下面这样 #一些说明 """ _i:子生成器,同时也是一个迭代器 _y:子生成器生产的值 _r:yield from 表达式最终的值 _s:调用方通过send()发送的值 _e:异常对象 """ _i = iter(EXPR) # EXPR是一个可迭代对象,_i其实是子生成器; try: _y = next(_i) # 预激子生成器,把产出的第一个值存在_y中; except StopIteration as _e: _r = _e.value # 如果抛出了`StopIteration`异常,那么就将异常对象的`value`属性保存到_r,这是最简单的情况的返回值; else: while 1: # 尝试执行这个循环,委托生成器会阻塞; _s = yield _y # 生产子生成器的值,等待调用方`send()`值,发送过来的值将保存在_s中; try: _y = _i.send(_s) # 转发_s,并且尝试向下执行; except StopIteration as _e: _r = _e.value # 如果子生成器抛出异常,那么就获取异常对象的`value`属性存到_r,退出循环,恢复委托生成器的运行; break RESULT = _r # _r就是整个yield from表达式返回的值。 """ 1. 子生成器可能只是一个迭代器,并不是一个作为协程的生成器,所以它不支持.throw()和.close()方法; 2. 如果子生成器支持.throw()和.close()方法,但是在子生成器内部,这两个方法都会抛出异常; 3. 调用方让子生成器自己抛出异常 4. 当调用方使用next()或者.send(None)时,都要在子生成器上调用next()函数,当调用方使用.send()发送非 None 值时,才调用子生成器的.send()方法; """ _i = iter(EXPR) try: _y = next(_i) except StopIteration as _e: _r = _e.value else: while 1: try: _s = yield _y except GeneratorExit as _e: try: _m = _i.close except AttributeError: pass else: _m() raise _e except BaseException as _e: _x = sys.exc_info() try: _m = _i.throw except AttributeError: raise _e else: try: _y = _m(*_x) except StopIteration as _e: _r = _e.value break else: try: if _s is None: _y = next(_i) else: _y = _i.send(_s) except StopIteration as _e: _r = _e.value break RESULT = _r """ 看完代码,我们总结一下关键点: 1. 子生成器生产的值,都是直接传给调用方的;调用方通过.send()发送的值都是直接传递给子生成器的;如果发送的是 None,会调用子生成器的__next__()方法,如果不是 None,会调用子生成器的.send()方法; 2. 子生成器退出的时候,最后的return EXPR,会触发一个StopIteration(EXPR)异常; 3. yield from表达式的值,是子生成器终止时,传递给StopIteration异常的第一个参数; 4. 如果调用的时候出现StopIteration异常,委托生成器会恢复运行,同时其他的异常会向上 "冒泡"; 5. 传入委托生成器的异常里,除了GeneratorExit之外,其他的所有异常全部传递给子生成器的.throw()方法;如果调用.throw()的时候出现了StopIteration异常,那么就恢复委托生成器的运行,其他的异常全部向上 "冒泡"; 6. 如果在委托生成器上调用.close()或传入GeneratorExit异常,会调用子生成器的.close()方法,没有的话就不调用。如果在调用.close()的时候抛出了异常,那么就向上 "冒泡",否则的话委托生成器会抛出GeneratorExit异常。 """
关键点:
- 1. 子生成器生产的值,都是直接传给调用方的;调用方通过.send()发送的值都是直接传递给子生成器的;如果发送的是 None,会调用子生成器的__next__()方法,如果不是 None,会调用子生成器的.send()方法;
- 2. 子生成器退出的时候,最后的return EXPR,会触发一个StopIteration(EXPR)异常;
- 3. yield from表达式的值,是子生成器终止时,传递给StopIteration异常的第一个参数;
- 4. 如果调用的时候出现StopIteration异常,委托生成器会恢复运行,同时其他的异常会向上 "冒泡";
- 5. 传入委托生成器的异常里,除了GeneratorExit之外,其他的所有异常全部传递给子生成器的.throw()方法;如果调用.throw()的时候出现了StopIteration异常,那么就恢复委托生成器的运行,其他的异常全部向上 "冒泡";
- 6. 如果在委托生成器上调用.close()或传入GeneratorExit异常,会调用子生成器的.close()方法,没有的话就不调用。如果在调用.close()的时候抛出了异常,那么就向上 "冒泡",否则的话委托生成器会抛出GeneratorExit异常
demo:
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 # (5700, [1200, 1500, 3000]) --total:5700 ; nums:[1200, 1500, 3000] def middle(key): while True: final_result[key] = yield from sales_sum(key) # 子生成器抛出异常StopIteration,会将return中的值:total, nums, 传回给 final_result 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协程 ,子生成器中执行next()方法 for value in data_set: # 委托生成器运行中止,以下都是调用方与子生成器的直接对接,直到数据yield完之后,抛出StopIteration异常,委托生成器拿回主控器,继续运行下面的代码,同时子生成器中return的值会传给委托生成器 m.send(value) # 调用方调用send方法,直接传到子生成器调用子生成器send()方法 m.send(None) # 手动抛出StopIteration异常,退出调用方与子生成器的直接对接,让委托生成器恢复运行后续代码 print("final_result:", final_result) # final_result:final_result: {'bobby牌面膜': (5700, [1200, 1500, 3000]), 'bobby牌手机': (289, [28, 55, 98, 108]), 'bobby牌大衣': (1688, [280, 560, 778, 70])} if __name__ == '__main__': main()
输出结果:
start key: bobby牌面膜 bobby牌面膜销量: 1200 bobby牌面膜销量: 1500 bobby牌面膜销量: 3000 bobby牌面膜销量: None bobby牌面膜销量统计完成!!. start key: bobby牌手机 bobby牌手机销量: 28 bobby牌手机销量: 55 bobby牌手机销量: 98 bobby牌手机销量: 108 bobby牌手机销量: None bobby牌手机销量统计完成!!. start key: bobby牌大衣 bobby牌大衣销量: 280 bobby牌大衣销量: 560 bobby牌大衣销量: 778 bobby牌大衣销量: 70 bobby牌大衣销量: None bobby牌大衣销量统计完成!!. final_result: {'bobby牌面膜': (5700, [1200, 1500, 3000]), 'bobby牌手机': (289, [28, 55, 98, 108]), 'bobby牌大衣': (1688, [280, 560, 778, 70])} Process finished with exit code 0
7、async和await
#python为了将语义变得更加明确,就引入了async和await关键词,用于定义原生的协程 async def downloader(url): return "hello" async def download_url(url): #dosomethings html = await downloader(url) return html if __name__ == "__main__": coro = download_url("http://www.imooc.com") coro.send(None)