协程,又称微线程,纤程。英文名Coroutine。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。协程最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
Python对协程的支持可以通过generator实现的,在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值,yield是一个关键字,含有该关键字的函数被称为generator,中文意思为生成器。
yield代码示例
import time
def consumer(name):
print("%s starting eating ..." %(name))
while True:
food = yield
print("[%s] is eating %s" % (name, food))
def producer():
for consumer in consumers:
consumer.__next__()
n = 0
while n < 5:
n += 1
for consumer in consumers:
consumer.send(n)
time.sleep(0.1)
print("[producer] is making %s" % n)
if __name__ == '__main__':
consumers = []
for i in range(10):
consumers.append(consumer("c" + str(i)))
p = producer()
遇到IO操作,就进行切换,可以实现执行效率的大幅提升。
我们可以通过greenlet,实现协程的主动切换。
greenlet代码示例
from greenlet import greenlet
def test1():
print(12)
gr2.switch()
print(34)
gr2.switch()
def test2():
print(56)
gr1.switch()
print(78)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
在上例中,启动了两个协程,然后切换到协程1执行,然后根据程序的定义,进行协程间的切换,直至程序执行完毕。
yield和greenlet都是手动切换,也就是主动切换,gevent可以实现自动切换,即在检测到IO或其他费时操作时会自动切换到其他的子程序。
gevent代码示例
import gevent
def foo():
print('Running in foo')
gevent.sleep(0.3)
print('context switch to foo again')
def foo2():
print('Running in foo2')
gevent.sleep(0.2)
print('context switch to foo2 again')
def foo3():
print("Running in foo3")
gevent.sleep(0.1)
print("context switch to foo3 again")
gevent.joinall([
gevent.spawn(foo),
gevent.spawn(foo2),
gevent.spawn(foo3),
])
在上例中,使用gevent启动了三个协程,让其分别执行,在协程内部出现IO等耗时操作时,会自动切换到其他的协程来执行,从而可以极大的提高cpu的利用率。该例子中,我们使用gevent.sleep模拟了IO操作。
如下使用一个例子使用gevent实现了协程操作,经过测试,使用协程方式从网站读取信息,比同步执行方式,可以节省50%以上的时间。
注:由于gevent无法直接判断出IO操作,需要使用monkey补丁才能行,所以导入monkey补丁,执行monkey.patch_all()。
from urllib import request
import gevent, time
from gevent import monkey
monkey.patch_all()
def f(url):
print('GET: %s' % url)
resp = request.urlopen(url)
data = resp.read()
print('%d bytes received from %s.' % (len(data), url))
urls = ['https://www.python.org/',
'https://www.yahoo.com/',
'https://github.com/' ]
time_start = time.time()
for url in urls:
f(url)
print("同步cost",time.time() - time_start)
coroutine_time_start = time.time()
gevent_jobs = []
for url in urls:
gevent_jobs.append(gevent.spawn(f, url))
gevent.joinall(gevent_jobs)
print("协程cost",time.time() - coroutine_time_start)
除了greenlet、yield、gevent可以实现协程,select也可以实现协程。
select的应用场景为IO多路复用,提供了select poll epoll三个方法(其中后两个在Linux中可用,windows仅支持select。
执行该代码时,会经历如下过程。
1)上下文切换转换为内核态
2)将fd(文件描述符)从用户空间复制到内核空间
3)内核遍历所有fd,查看其对应事件是否发生
4)如果没发生,将进程阻塞,当设备驱动产生中断或者timeout时间后,将进程唤醒,再次进行遍历
5)返回遍历后的fd
6)将fd从内核空间复制到用户空间
select代码示例
import select
import socket
import queue
server = socket.socket()
server.bind(('localhost', 9000))
server.listen(1000)
server.setblocking(False)
msg_dic = {}
inputs = [server,]
outputs = []
while True:
readable, writeable, exceptional= select.select(inputs, outputs, inputs)
print(readable, writeable, exceptional)
for r in readable:
if r is server:
conn, addr = server.accept()
print("新连接", addr)
inputs.append(conn)
msg_dic[conn] = queue.Queue()
else:
data = r.recv(1024)
print("收到数据", data)
msg_dic[r].put(data)
outputs.append(r)
for w in writeable:
data_to_client = msg_dic[w].get()
w.send(data_to_client)
outputs.remove(w)
for e in exceptional:
if e in outputs:
outputs.remove(e)
inputs.remove(e)
del msg_dic[e]
客户端演示代码如下。
import socket
HOST = 'localhost'
PORT = 9000
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
msg = bytes(input(">>:"), encoding = "utf8")
s.sendall(msg)
data = s.recv(1024)
print('Received', data)
s.close()