主要内容:
1. yeild 实现状态保存
import time def func(): sum = 0 yield sum sum = 0 yield sum sum = 0 yield sum def fff(): g = func() # 获得一个生成器函数, 并不会执行函数 print('这是在fff函数中') print(next(g)) # 执行 time.sleep(1) print('这是在fff函数中') print(next(g)) time.sleep(1) print('这是在fff函数中') print(next(g)) fff()
2 . yield 实现并发的假象
在单线程中, 如果存在多个函数, 如果某个函数存在i/o操作, 想让程序马上切换到另一个函数去执行,以此实现一个假的并发现象.
总结 : yield 只能实现单纯的切换函数和保存函数状态的功能
不能实现: 当某一个函数遇到i/o操作时, 自动的切换到另一个函数去执行, 如果能实现这个功能, 那么每个函数都是一个协程.
但是协程的本质还是主要依靠yield实现的
如果只是拿yield去单纯的实现一个切换的现象, 根本没有程序串行执行效率高.
def consumer(): while 1: x = yield # print(x) def producer(): g = consumer() next(g) for i in range(100000000): g.send(i) start = time.time() producer() print('yield:',time.time() - start)
串行代码:
def consumer(l): # for i in l: # print(i) pass def producer(): l = [] for i in range(100000000): l.append(i) return l start = time.time() l = producer() consumer(l) print(time.time() - start)
3 . 协程
a : 协程的定义 : 是一个比线程更加轻量级的单位, 是组成线程的各个函数,(单线程下的并发, 又称)
b : 为什么要有协程 因为想要在单线程内实现并发的效果(因为cpython有GIL锁,限制了在同一个时间点,只能执行一个线程. 所以想要在执行一个线程的期间, 充分利用cpu的性能, 所以想在单线程内实现并发的效果)
c : 并发 : 切换 + 保存状态
d : cpu为什么要切换: 1.因为某个程序阻塞了. 2. 因为某个程序用完了时间片(该任务计算的时间过长)
e : 目标 : 所以想要实现单线程的并发, 就要解决在单线程内,多个任务函数中,某个任务函数遇到i/o操作, 马上自动切换到其他任务函数中去执行
协程是用户自己去调度的
3 . greenlet模块
a :定义 能简单的实现函数与函数的切换, 但是遇到i/o操作, 不能自动切换.
b :模块的使用 : 该模块是一个类, switch是类中的一个方法.
注册一下函数func, 将函数注册成一个对象f1 f1 = greenlet(func)
调用func, 使用f1.switch(), 如果函数需要传参, 就在switch这里传参即可.
from greenlet import greenlet import time def eat(name): print('%s吃炸鸡' % name) time.sleep(1) f2.switch('lili') print('%s吃雪糕' % name) f2.switch() def drink(name): print('%s喝啤酒' % name) time.sleep(1) f1.switch() print('%s喝可乐' % name) f1 = greenlet(eat) f2 = greenlet(drink) f1.switch('丽丽')
4. gevent 模块
a : 定义 可以实现在某函数内部遇到io操作,就自动的切换到其他函数内部去执行
b : 模块的使用 : g = gevent.spawn(func,参数) 注册一下函数func,返回一个对象g
gevent.join(g) #等待g指向的函数func执行完毕,如果在执行过程中,遇到IO,就切换
gevent.joinall([g1,g2,g3])#等待g1 g2 g3指向的函数func执行完毕
import gevent def func1(): print(1) gevent.sleep(0.5) print(2) def func2(): print(3) gevent.sleep(0.5) #gevent不能识别其他的io操作,只能识别自己的 print(4) g = gevent.spawn(func1) g2 = gevent.spawn(func2) g.join() g2.join()
解决gevent不能识别其他io操作的问题
import gevent
from gevent import monkey
monkey.patch_all()# 可以让gevent识别大部分常用的IO操作
import time
def func1():
print('1 2 3 4')
time.sleep(1)
print('3 2 3 4')
# gevent.sleep(1)
def func2():
print('2 2 3 4')
time.sleep(1)
print('再来一次')
g1 = gevent.spawn(func1)
g2 = gevent.spawn(func2)
g1.join()# 等待g1指向的任务执行结束
g2.join()
串行与并发的效率对比:
from gevent import monkey monkey.patch_all() import gevent import time def func(num): time.sleep(1) print(num) start = time.time() for i in range(10): func(i) print('时间', time.time() - start) #10s if __name__ == '__main__': li = [] start = time.time() for i in range(10): g = gevent.spawn(func, i) li.append(g) gevent.joinall(li) # 等待g指向的函数执行完毕. print('时间',time.time() - start) #1s
爬虫事例 :
from gevent import monkey import time import requests import gevent def func(url): re = requests.get(url) print(url, re.status_code, len(re.text)) url_l = ['http://www.baidu.com', 'https://www.jd.com', 'http://www.taobao.com', 'http://www.qq.com', 'http://www.mi.com', 'http://www.cnblogs.com'] def sync_func(url_l): for url in url_l: func(url) #串行执行函数 def async_func(url_l): li = [] for url in url_l: g = gevent.spawn(func, url) #使用gevent协程并发去执行任务函数 #当遇到每个网页请求比较大的网络延迟时,自动切换到其他的任务函数. li.append(g) gevent.joinall(li) #等待g指向的任务函数执行完. start = time.time() sync_func(url_l) print('使用串行消耗的时间为', time.time() - start) start = time.time() async_func(url_l) print('使用并发消耗的时间为', time.time() - start)
5 . i/o多路复用
a : 用非阻塞io模型去解决阻塞io
import socket sk = socket.socket() sk.setblocking(False) sk.bind(('127.0.0.1',8080)) sk.listen() l = [] del_l = [] while 1: try: conn,addr = sk.accept()# 如果是阻塞IO模型,在这里程序会一直等待。 l.append(conn)# 将每个请求连接的客户端的conn添加到列表中 except BlockingIOError: for conn in l:# 去遍历所有客户端的conn,看看有没有客户端给我发送数据了 try: info = conn.recv(1024).decode('utf-8')# 尝试接收,看看有没有客户端给我发数据 if not info:# 如果客户端正常执行了close,服务器会接收到一个空 del_l.append(conn)# 将已经结束的客户端的conn,添加到要删除的列表中 print('客户端正常退出了!') conn.close()# 因为客户端已经主动close,所以服务器端的conn也要close else: print(info) conn.send(info.upper().encode('utf-8')) except BlockingIOError: continue# 是没有接受到客户端发来的数据而报错 except ConnectionResetError: pass# 是因为客户端强制退出而报错 if del_l: for conn in del_l: l.remove(conn) del_l = []# 在删除完主动关闭的客户端的连接之后,应该把此列表清空,否则报错
基于select的网络io模型:
import select import socket sk = socket.socket() sk.bind(('127.0.0.1',8080)) sk.listen() del_l = [] rlist = [sk]# 是用来让select帮忙监听的 所有 接口 # select:windows/linux是监听事件有没有数据到来 # poll: linux 也可以做select的工作 # epoll: linux 也可以做类似的工作 while 1: r,w,x = select.select(rlist,[],[])# 传参给select,当rlist列表中哪个接口有反应,就返回给r这个列表 if r: for i in r:# 循环遍历r,看看有反应的接口到底是sk 还是conn if i == sk: # 如果是sk,那就表示有客户端的连接请求 '''sk有数据要接收,代表着有客户端要来连接''' conn,addr = i.accept() rlist.append(conn)# 把新的客户端的连接,添加到rlist,继续让select帮忙监听 else: # 如果是conn,就表示有客户端给我发数据了 '''conn有数据要接收,代表要使用recv''' try: msg_r = i.recv(1024).decode('utf-8') if not msg_r: '''客户端执行了close,客户端主动正常关闭连接''' del_l.append(i) i.close() else: print(msg_r) i.send(msg_r.upper().encode('utf-8')) except ConnectionResetError: pass if del_l:# 删除那些主动断开连接的客户端的conn for conn in del_l: rlist.remove(conn) del_l.clear()
i/o 多路复用 : 阻塞i/o ; 非阻塞i/o ; 多路复用i/o ; 异步i/o : python实现不了, 但是有tornado框架,天生自带异步.
6 . 知识点总结
1 ) 进程 , 线程, 协程的区别及各自的应用场景
计算密集用多进程, 可以充分利用多核cpu的性能
i/o密集用多线程(注意 , 协程是在单线程的)
多线程和协程的区别:线程由操作系统调度控制的; 协程是由程序员自己调度控制.
2 ) select 和 poll 和epoll 的区别
select和poll有一个共同的机制, 都是采用轮训的方式去询问内核,有没有数据准备好了;
select有一个最大监听事件的限制, 32位机制1024, 6位机制2048
poll 没有 , 理论上poll可以开启无限大, 1G内存大概可以开10w个事件去监听
epoll是最好的, 采用的是回调机制, 解决了select和poll共同存在的问题而且poll可以开启无限多个监听事件.