协程
一、协程概念
1、生成器
def fun(x):
while True:
y = (yield) # yield也单独使用,可以加括号,也可以不加
print(y,'****')
f = fun(1)
next(f)
f.send(123) # 使用生成器可以实现反复的输入和输出
#这里输入和输出可以看成数据间的收发
genertor
通过生成器来实现对函数的反复输入和输出,这样的输入和输出同样可以用来传递数据
2、消费者和生产者模型
import time
def consumer():
'''消费者,接收数据,处理数据'''
while True:
x = yield
# time.sleep(3) # 这里只是进行了切换,但是并没有节省I/O时间
print('处理了数据:',x)
def producer():
'''生产者,产生数据'''
g = consumer()
next(g)
for i in range(20):
# time.sleep(2)
g.send(i)
# 给yield传值,然后再循环给下一个yield传值,并且多了切换的程序
# 比直接串行执行还多了一些步骤,导致执行效率反而更低了
print('发送了数据:',i)
# 基于yield保存状态,实现两个任务直接来回切换,即并发的效果
# PS:如果每个任务都加上打印,那么明显地看到两个任务的打印是你一次我一次,即并发执行的
producer() # 在当前线程中只执行了这个函数,但是通过这个函数里面的send切换了另外一个任务
这里的生产者和消费者既不是进程实现,也不是线程实现,而是两个生成器之间相互协同完成的,这种就称为协程
3、协程的概念
3-1、名词解释
协程:是单线程下的并发
,又称微线程。
协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
3-2、优点
1、协程的切换开销更小
,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2、单线程内就可以实现并发的效果,最大限度地使用CPU
3-3、缺点
1、协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
2、协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
3-4、特点
1、必须在只有一个单线程里实现并发
2、修改共享数据不需加锁
3、用户程序里自己保存多个控制流的上下文栈
4、附加:一个协程遇到IO操作自己切换到其他协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
二、greenlet
import time
from greenlet import greenlet
def producer():
for i in range(20):
print('生产了 %s' % i)
con.switch(i) # 切换到消费者
time.sleep(1) # 还是会等待,greenlet只是实现了切换
print('第%s次生产消费完成' % i)
def consumer():
while True:
var = pro.switch() # 等待切换过来和传入值
print('消费了%s' % var) # 在等待的时候可以先干点其他的事情
con = greenlet(consumer) # 真正的协程由greenlet实现
pro = greenlet(producer)
con.switch() # 消费者进入等待状态
第三方模块,需要安装
pip install greenlet
greenlet
生成器实现协程非常不方便,尤其很多协程的时候,使用greenlet就要方便很多
而这个模块来自python的一个衍生版stackless Python原生的协程(常见是标准Python是cpython),将其协程单独拿出来打包成模块,因此性能要比生成器强很多
注意
这里并没有解决IO阻塞的问题,但是我们使用这个时间来做别的事情了,一般在工作中我们都是进程+线程+协程的方式来实现并发,以达到最好的并发效果
三、gevent
1、gevent实现并发服务器
from gevent import monkey;monkey.patch_all() # 必须放到被打补丁者的前面,如time,socket模块之前,all是指可以所有切换成协程的内容
# 猴子补丁,就是在代码中临时改变对象的属性和方法
import gevent
import socket
server = socket.socket()
server.bind(('127.0.0.1',8989))
server.listen(1000)
def fun_coroutimes(conn):
while True:
recv_data = conn.recv(1024)
if recv_data:
print(recv_data)
conn.send(recv_data)
else:
conn.close()
break
while True:
conn,addr = server.accept()
# 生成一个协程,并将conn作为参数传入
gevent.spawn(fun_coroutimes,conn)
gevent
gevent封装了epoll和greenlet,在使用的时候要更加方便
同时实现了IO阻塞时的自动切换
2、网络编程小结
一切为了让cpu忙起来
如果阻塞在IO,则可以使用epoll
如果一个进程忙计算不过来,对于多核CPU则可以使用多进程,但是开销大,由操作系统调度
如果IO比较密集,则可以使用多线程,因为GIL锁存在,所以都是并发执行,且要注意资源抢占情况,开销适中,由Python解释器调度
协程IO阻塞时,可以自己控制调度到其他的协程,开销最小,使用gevent可以实现IO异步
四、gevent应用
1、实现通信
from gevent import monkey;monkey.patch_all()
from gevent.queue import Queue
import gevent
queue = Queue(3)
def producer(queue):
for i in range(20):
print(f'生产了{i}')
queue.put(i)
def consumer(queue):
for i in range(20):
var = queue.get()
print(f'消费了{var}')
# gebent自动切换,不需要我们使用switch去切换
pro = gevent.spawn(producer,queue)
con = gevent.spawn(consumer,queue)
gevent.joinall([pro,con]) # 等待所有协程结束
2、实现异步
from gevent import spawn,joinall,monkey
monkey.patch_all()
import time
def task(pid):
time.sleep(0.5) # 模拟阻塞
print(f'Task {pid} done')
def synchronous():
for i in range(10):
task(i)
def asynchronous():
g_l = [spawn(task,i) for i in range(10)] # 初始化列表
joinall(g_l) # 等待所有协程执行完
if __name__ == '__main__':
print('Synchronous:')
start = time.time()
synchronous() # 同步方法
print(f'synchronous used {time.time()-start}')
print('Asynchronous:')
start = time.time()
asynchronous() # 异步方法
print(f'aynchronous used {time.time()-start}')
同步:所有的方法都是依次执行,总花费时间是所有方法运行之和
异步:与同步相对应,异步指的是让CPU暂时搁置当前请求的响应,处理下一个请求,当通过轮询或其他方式得到的回调通知后,开始运行。
多任务是将异步放在子任务中完成。