协程
协程,又叫微线程、纤程,可以认为是比线程更小的执行单元。协程自带CPU上下文,这样只要在合适的时机,我们可以把一个协程切换到另一个协程,只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。
通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定
协程和线程差异
最大的优势就是协程极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销
线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。
操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,
操作系统还会帮你做这些数据的恢复操作。所以线程的切换非常耗性能。
但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,利用多核CPU,最简单的方法是多进程+协程
Python对协程的支持是通过generator(生成器)实现的。
协程的问题
但是协程有一个问题,就是系统并不感知,所以操作系统不会帮你做切换。
那么谁来帮你做切换?让需要执行的协程更多的获得CPU时间才是问题的关键。
先来一个传统的函数调用,让A,B函数都运行起来
def A():
while True:
print("=====A=====")
time.sleep(1)
def B():
while True:
print("=====B=====")
time.sleep(1)
if __name__ == '__main__':
A()
B()
执行结果:
我想让A B函数都执行,结果呢,A在运行时,B函数一直处于等待状态
import threading
def A():
while True:
print("=====A=====")
time.sleep(1)
B()
def B():
while True:
print("=====B=====")
time.sleep(1)
A()
if __name__ == '__main__':
A()
B()
哪怕我在A函数中调用B函数,B函数依旧无法执行,执行结果如上
用线程来解决AB函数无法同时调用的问题
import threading
# 实现A、B交替打印
def A():
while True:
print("=====A=====")
time.sleep(1)
# B()
def B():
while True:
print("=====B=====")
time.sleep(1)
# A()
if __name__ == '__main__':
a = threading.Thread(target=A)
b = threading.Thread(target=B)
a.start()
b.start()
执行结果:
这样,AB函数都会执行了
如果想要AB函数交替执行,在A函数中调用B,B函数中调用A即可
用协程解决AB函数无法同时调用的问题
import time
#A函数是一个generator
def A():
while True:
print("----A---")
yield
time.sleep(0.5)
def B(a):
while True:
print("----B---")
next(a)
time.sleep(0.5)
if __name__ == '__main__':
a = A()
B(a)
执行结果:
先B后A,依次执行
用协程做生产者-消费者模型
def consumer(): # consumer函数是一个generator
while True:
n = yield "ok" #生成式,获取一个ok值
print('[消费者] 消费了 %s...' % n)
def produce(c):
c.send(None) # 启动生成器
n = 0
while n < 5:
n = n + 1
print('[生产者] 生产了 %s...' % n)
r = c.send(n)
# c.send(),即consumer().send(),输出的结果为print('[消费者] 消费了 %s...' % n)的值,即r的值
# n的值传进consumer(),不调用生成器,print的结果
print(n)
print(r)
print('[生产者] 消费者结果: %s' % r)
c.close()
if __name__ == '__main__':
c = consumer()
produce(c)
执行结果:
使用greenlet + switch实现协程调度
from greenlet import greenlet
import time
def func1():
print("我爱你")
time.sleep(1)
gr2.switch() # 把CPU执行权交给gr2
print("分手吧")
time.sleep(1)
gr2.switch()
pass
def func2():
print("爱着你")
time.sleep(1)
gr1.switch()
print("不爱了")
pass
if __name__ == '__main__':
gr1 = greenlet(func1)
gr2 = greenlet(func2)
gr1.switch()
执行结果:
执行过程:
先用gr1,调用func1(), 执行print(“我爱你”) ,然后
-------- 交给gr2 ----------
用gr2 ,调用func2(), 执行print(“爱着你”) ,然后
-------- 交给gr1 ----------
用gr1 ,调用func1(),执行print(“分手吧”) ,然后
-------- 交给gr2 ----------
用gr2 ,调用func2(),执行print(“不爱了”) ,结束
使用gevent + sleep自动将CPU执行权分配给当前未睡眠的协程
import gevent
from gevent import Greenlet
def func1():
gevent.sleep(5)
print("离离原上草")
gevent.sleep(10)
print("草真多")
pass
def func2():
gevent.sleep(1)
print("一岁一枯荣")
gevent.sleep(5)
print("烧没了")
pass
def simpleGevent():
gr1 = gevent.spawn(func1)
gr2 = gevent.spawn(func2)
gevent.joinall([
gr1, gr2
])
if __name__ == '__main__':
simpleGevent()
执行结果:
执行过程:
按照先gr1,后gr2的顺序开始计时
1, print(“一岁一枯荣”) sleep时间1秒, print(“离离原上草”) sleep时间5秒,故执行 一岁一枯荣
2,在第一次print后重新计时
以print(“一岁一枯荣”)的时间开始计时,print(“烧没了”) sleep时间5秒,即打印时间为第1+5秒
以print(“离离原上草”)的时间开始计时,print(“草真多”) sleep时间10秒,即打印时间为第5+10秒
按照先gr1,后gr2的次序,以时间为顺序打印
同一函数内的打印顺序不变,自上而下一次执行
一个很有意思的小玩意,可以帮你更好的理解协程
import gevent
answers = {
"我们是什么": "浏览器",
"我们要什么": "速度",
"什么时候要": "现在",
"还有问题吗": None,
}
def firefox(question):
gevent.sleep(1)
print("firefox:", answers[question])
pass
def opera(question):
gevent.sleep(1)
print("opera:", answers[question])
pass
def safari(question):
gevent.sleep(1)
print("safari:", answers[question])
pass
def ie(question):
gevent.sleep(10)
print("ie:", answers[question])
pass
if __name__ == '__main__':
q1 = "我们是什么"
q2 = "我们要什么"
q3 = "什么时候要"
q4 = "还有问题吗"
print(q1)
gevent.joinall([
gevent.spawn(ie, q1),
gevent.spawn(firefox, q1),
gevent.spawn(opera, q1),
gevent.spawn(safari, q1)
], timeout=2)
print("----------\n")
print(q2)
gevent.joinall([
gevent.spawn(ie, q2),
gevent.spawn(firefox, q2),
gevent.spawn(opera, q2),
gevent.spawn(safari, q2)
], timeout=2)
print("----------\n")
print(q3)
gevent.joinall([
gevent.spawn(ie, q3),
gevent.spawn(firefox, q3),
gevent.spawn(opera, q3),
gevent.spawn(safari, q3)
], timeout=2)
print("----------\n")
print(q4)
gevent.joinall([
gevent.spawn(ie, q4),
gevent.spawn(firefox, q4),
gevent.spawn(opera, q4),
gevent.spawn(safari, q4)
], timeout=5)
print("----------\n")
pass
使用gevent + monkey.patch_all()自动调度网络IO协程
import gevent
import requests
import time
from gevent import monkey
def getPageText(url, order=0):
print("No%d:%s请求开始..." % (order, url))
resp = requests.get(url) # 发起网络请求,返回需要时间——阻塞IO
html = resp.text
print("No%d:%s成功返回:长度为%d" % (order, url, len(html)))
pass
# 将【标准库-阻塞IO实现】替换为【gevent-非阻塞IO实现】
monkey.patch_all()
if __name__ == '__main__':
start = time.time()
# time.clock()
gevent.joinall([
gevent.spawn(getPageText, "http://www.sina.com", order=1),
gevent.spawn(getPageText, "http://www.baidu.com", order=2),
])
end = time.time()
print("over,耗时%d秒"%(end - start)) 非阻塞实现,耗时0秒哦
# print(time.clock())
pass