python协程
协程又叫做微线程,英文名叫Coroutine。与线程一样的是协程也相对独立,有自己的上下文,可相互切换,不同的是线程由解释器调用cpu来实现,而协程由程序自身控制。与线程一样,协程也可以实现生产者与消费者模式和实现并发。多协程与多线程相比,最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。协程本身不能避开阻塞,任意时刻只有一个协程在执行。
协程引入
我们知道,一个函数有唯一的传入时机和唯一的传出时机,其传入时机是在调用函数传参的时候,而传出时机是在函数返回值返回的时候。由这种方式,我们又可以引入 生成器 的概念,与函数不同的是,它可以有多次传出时机,利用yield可以做到暂停函数,并返回值,并在遇到next后可继续执行函数,直到遇到下一个yield。
def func(q):
print("---start---")
for i in range(100):
yield i
a = func(666)
print(a)
print(next(a))
print(next(a))
print(next(a))
print(next(a))
<generator object func at 0x000001FAE51A0AF0>
---start---
0
1
2
3
可以看出,生成器直接调用不会马上执行,而是返回一个生成器对象,只有遇到next才可以开启生成器,遇到第一个yield会返回一个值,并暂停,当再次遇到next会恢复执行。故可以通过yield实现多次返回值。
那么有没有可以实现多个入口,多个出口的函数呢?答案是有的,这种函数我们叫做协程,他可以通过yield多次返回值,又可以通过send多次传入值。在python2.5以后,加入了yield表达式,既可以在恢复的时候接收值。yield可以不返回值,但这时需要给yield加上括号。这里要注意的是,恢复yield不仅可以是next,也可以是send,但开启函数一定只能是next。
def func():
print("---start---")
for i in range(100):
a = (yield) #yield接收值,给a
print(a)
a = func()
print(a)
print(next(a)) #开启协程
a.send("hello")
a.send('world')
a.send(666)
a.send(888)
<generator object func at 0x0000023FB6130AF0>
---start---
None
hello
world
666
888
模拟生产者和消费者模式
利用协程和主程序,主程序函数充当生产者,协程充当消费者。
import random
def product(comsume): #生产者
next(comsume) #开启协程
while True:
item = random.randint(0,100)
comsume.send(item)
print("生产了%d"%item)
def comsume(): #消费者
while True:
x = (yield)
print("消费了%d"%x)
a = comsume() #返回一个生成器对象
product(a)
生产了82
消费了34
生产了34
消费了79
生产了79
消费了26
生产了26
消费了6
生产了6
.......
以上代码,实际上实现了两个函数的来回切换。
协程包greenlet
greenlet是底层实现了原生协程的c扩展库,由于yield和send的语义不明确,在greenlet换成了switch。switch既可以充当send来传递值,也可以充当yield来暂停函数,并且还实现了协程之间的切换,切换的对象是switch前的生成器名。
from greenlet import greenlet
from random import randint
def produce():
while True:
item = randint(1,100)
print("生产了%d"%item)
c.switch(item) #切换到协程c,并传递一个item值
def consumer():
while True:
a = p.switch() #收到item值并赋给a,并切换到协程p
print("消费了%d"%a)
c = greenlet(consumer) #将函数封装成一个协程
p = greenlet(produce)
c.switch() #开启协程c
生产了63
消费了63
生产了5
消费了5
生产了44
消费了44
生产了85
消费了85
生产了4
消费了4
生产了17
......
gevent
gevent通过封装libev(基于epoll)和greenlet两个库,使得协程既可以避开阻塞,也可以向类似于线程的方式那样切换执行,但这种协程没有轮询的开销,也没有线程的开销,故其效率很高。与greenlet、eventlet相比,性能略低,但是它封装的API非常完善,最赞的是提供了一个monkey类,如mokey.patch_socket()这个补丁可以将python自带的socket套接字替换成一个自己封装了epoll的套接字,这个套接字,可以在遇到阻塞的时候自动切换协程。
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
gevent是基于greenlet的,所有自然可以用switch来切换,但sevent的优势在于不需要手动切换,所以一般在gevent模块下不用switch。下面实现手动切换和自动切换的协程通信。
手动切换
from gevent import spawn
from random import randint
def produce():
while True:
item = randint(1,100)
print("生产了%d"%item)
c.switch(item)
def consumer():
while True:
a = p.switch()
print("消费了%d"%a)
c = spawn(consumer) #封装成协程
p = spawn(produce)
c.join() #join等待
p.join()
生产了100
消费了100
生产了11
消费了11
生产了22
消费了22
生产了76
消费了76
生产了3
消费了3
生产了34
消费了34
生产了12
......
自动切换
from gevent import monkey;monkey.patch_all() #打上猴子补丁
from gevent import spawn,joinall,queue
from random import randint
def produce(que):
while True:
item = randint(1,100)
print("生产了%d"%item)
que.put(item)
def consumer(que):
while True:
a = que.get()
print("消费了%d"%a)
que = queue.Queue(3)
c = spawn(consumer,que) #封装成协程
p = spawn(produce,que)
joinall([c,p])
生产了42
生产了18
生产了74
消费了30
消费了42
消费了18
生产了70
生产了6
生产了67
消费了74
消费了70
消费了6
......
实现并发通信
from gevent import spawn
from gevent import monkey;monkey.patch_socket() #打上猴子补丁
import socket
def acce(coon,addr):
while True:
data = coon.recv(1024)
if data:
print("收到来自{}的信息{}".format(addr,data.decode()))
coon.send(data)
else:
coon.close()
break
server = socket.socket()
server.bind(('127.0.0.8',8520))
server.listen(20)
while True:
coon,addr = server.accept()
spawn(acce,coon,addr) #封装成协程,并传参
import socket
client = socket.socket()
client.connect(('127.0.0.8',8520))
data = input("--->")
client.send(data.encode())
print(client.recv(1024))
client.close()
收到来自('127.0.0.1', 54608)的信息afdsa
收到来自('127.0.0.1', 54613)的信息afasdga
收到来自('127.0.0.1', 54614)的信息fafa
收到来自('127.0.0.1', 54611)的信息faf
收到来自('127.0.0.1', 54610)的信息agdgx