协程
一.协程
1.了解协程
(1)并发:切换 + 保存状态(程序停止等待)
(2)协程的含义:
协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的,是组成线程的各个函数
协程本身没有实体,协程是在单线程的
(3)为什么要有协程?
因为想要在单线程内实现并发的效果.
因为Cpython中有GIL锁,限制了在同一个时间点,只能执行一个线程,所有想要在执行一个线程的期间,充分利用CPU的性能,所以才有了想在单线程内饰并发的效果
(4)CPU为什么要切换?
①因为某个程序阻塞了 ②因为某个程序用完了时间片
很明显解决①这个问题才能提高效率,所以想要实现单线程的并发,就要解决在单线程内,多个任务函数中,某个任务函数遇见IO操作,马上自动切换到其它任务函数去执行
2.yield生成器的特点
yield生成器自带保存状态
例如:
def consumer():
while 1:
x = yield
print(x)
def product():
g = consumer() #实例化一个对象 调用consumer函数,此时不执行consumer函数
next(g) #此时执行consumer函数,但是执行到x = yield的时候就停止了,因为没有参数
for i in range(100): #循环遍历
g.send(i) #为yield传值 传完值之后,光标就在yield后面停止 等到下次执行的时候,继续先从x执行
print("这是product函数")
product() #首先调用product函数
例如:
def func():
sum = 0
while 1:
yield sum
g = func() #当程序执行到这里的时候,并不会执行func函数
print(next(g)) #遇到第一个next(g)的时候从头到尾执行函数,一直执行到yield,然后光标停在yield后面
print(next(g)) #第二次执行的时候,就直接从while循环开始执行
总结:
(1)yield只能实现单纯的切换函数和保存函数的状态的功能,不能够实现当某一个函数遇到IO操作阻塞时,自动地切换到另一个函数去执行
(2)变成协程的目标:当某一个函数遇到IO操作阻塞时候,会自动地切换到另一个函数去执行,如果能实现这个功能,那么每一个函数都是一个协程,但是协程的本质还是依靠于yield实现的
(3)如果只是拿yield去单纯的实现一个切换现象,你就会发现根本没有程序串行执行效率高
3.greenlet模块
能简单的实现函数与函数之间的切换,但是遇到IO操作,不能自动切换到其他函数中
(1)注册一下,函数func,将函数注册成一个对象f
f = greenlet(func)
(2)调用func,使用f.switch(),如果func需要传参,就在switch这里传参即可
当使用switch调用函数func 的时候,什么时候func会停止?
(1)要么return (2)要么在func内部又遇到了switch
例如:
from greenlet import greenlet
def eat(name):
print("%s吃炸鸡" % name) #执行这句话
f2.switch("牛哞哞") #使用f2.switch调用drink这个函数,并为其传一个名字的参数
print("%s吃蛋糕" % name)
f2.switch() #又使用f2.switch调用drink这个函数
def drink(name):
print("%s喝啤酒" % name) #执行这句话
f1.switch() #又使用f1.switch调用eat函数
print("%s喝可乐" % name)
f1 = greenlet(eat) #首先注册了两个对象,分别是f1和f2
f2 = greenlet(drink)
f1.switch("刘某某") #使用f1.switch调用eat这个函数,并为其传一个名字的参数
4.gevent模块
可以实现在某函数内部遇到IO操作阻塞,就会自动的切换到其它函数内部去执行
g = gevent.spawn(func,参数)
g.join() 让func函数执行完毕
gevent.join([g1,g2,g3]) 让多个函数执行完毕
func停止的原因:
(1)func执行完毕 (2)遇到IO操作的时候
(1)例如:
import gevent
def func():
print("1 2 3 4")
gevent.sleep(1) #遇到IO操作,自动执行另一个函数
print("3 2 3 4")
gevent.sleep(1) #此时gevent不能识别到其它的IO操作,只能识别自己认识的IO操作(gevent.sleep())
def func1():
print("2 2 3 4")
gevent.sleep(1) #再次遇到IO操作,又回到刚才那个函数去执行
print("再来一次")
g1 = gevent.spawn(func) #先注册一个func,将func注册成一个对象g1和g2
g2 = gevent.spawn(func1)
g1.join() #通过g1对象执行func函数
(2)例如:解决gevent不能识别其它的IO操作
from gevent import monkey
import gevent
monkey.patch_all() #可以让gevent识别大部分常用的IO操作
import time
def func():
print("1 2 3 4")
time.sleep(1)
print("3 2 3 4")
def func1():
print("2 2 3 4")
time.sleep(1)
print("再来一次")
g1 = gevent.spawn(func)
g2 = gevent.spawn(func1)
g1.join()
g2.join()
(3)例如:串行和并发的效率对比
并发效率比串行快
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)
start = time.time()
l = []
for i in range(10):
g = gevent.spawn(func,i) #协程去并发实现执行任务函数
l.append(g)
gevent.joinall(l) #等待l里面的全部函数执行完毕
print(time.time() - start)
(4)爬虫实例
from gevent import monkey
monkey.patch_all()
import gevent
import time
import requests
def get_result(url):
res = requests.get(url)
print(url,res.status_code,len(res.text))
url_l = ['http://www.baidu.com',
'https://www.jd.com',
'http://www.apache.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:
get_result(url)
def async_func(url_l):
'''异步'''
l = []
for url in url_l:
l.append(gevent.spawn(get_result,url))
gevent.joinall(l)
start = time.time()
sync_func(url_l)
print('sync:',time.time() - start)
start = time.time()
async_func(url_l)
print('async:',time.time() - start)
5.大的总结 (面试题)
(1)协程是由用户自己去调度的
(2)计算密集用多进程,可以充分利用多核CPU的性能
(3)IO密集用多线程(协程是单线程的)
(4)多线程和协程的区别:
①线程是由操作系统调度,控制
②协程是由程序员自己调度,控制
二.IO多路复用
1.阻塞IO
2.非阻塞IO
3.多路复用IO
4.异步IO (python实现不了,但是有tornado框架,天生自带异步)
例子一:用非阻塞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 = []# 在删除完主动关闭的客户端的连接之后,应该把此列表清空,否则报错
客户端代码:
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
while 1:
msg_s = input('>>>')
if not msg_s:continue
if msg_s == 'q':break
sk.send(msg_s.encode('utf-8'))
print(sk.recv(1024).decode('utf-8'))
sk.close()
例子二:基于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()
客户端代码:
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
while 1:
msg_s = input('>>>')
if not msg_s:continue
if msg_s == 'q':break
sk.send(msg_s.encode('utf-8'))
print(sk.recv(1024).decode('utf-8'))
sk.close()
面试题
select 和 poll 和 epoll区别
(1)select 和 poll 有一个共同的机制,都是采用轮询的方式去询问内核,有没有数据准备好了
(2)select 有一个最大监听事件的限制,32位机限制1024,6位机限制2048
(3)poll没有,理论上poll可以开启无限大,1G内存大概够你开10W个事件去监听
(4)epoll是最好的,采用的是回调机制,解决了select和poll共同存在的问题
而且epoll理论上也可以开启无限多个监听事件