什么是协程
协程:协助程序,线程和进程都是抢占式特点,线程和进程的切换我们是不能参与的。
而协程是非抢占式特点,协程也存在着切换,这种切换是由我们用户来控制的。
协程主解决的是IO的操作。
协程,又称微线程,纤程。英文名Coroutine。
优点1: 协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因
此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
优点2: 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中
控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
因为协程是一个线程执行,那怎么利用多核CPU呢?简单的方法是多进程+协程,既充分利
用多核,又充分发挥协程的高效率,可获得极高的性能。
协程的形象理解
协程的原理很简单,打个比方就能讲明白了:假设说有十个人去食堂打饭,这个食堂比较穷,只有一个打饭的窗口,并且也只有一个打饭阿姨,那么打饭就只能一个一个排队来打咯。这十个人胃口很大,每个人都要点5个菜,但这十个人又有个毛病就是做事情都犹豫不决,所以点菜的时候就会站在那里,每点一个菜后都会想下一个菜点啥,因此后面的人等的很着急呀。这样一直站着也不是个事情吧,所以打菜的阿姨看到某个人犹豫5秒后就开始吼一声,会让他排到队伍最后去,先让别人打菜,等轮到他的时候他也差不多想好吃啥了。这确实是个不错的方法,但也有一个缺点,那就是打菜的阿姨会等每个人5秒钟,如果那个人在5秒内没有做出决定吃啥,其实这5秒就是浪费了。一个人点一个菜就是浪费5秒,十个人每个人点5个菜可就浪费的多啦(菜都凉了要)。那咋办呢?
这个时候阿姨发话了:大家都是学生,学生就要自觉,我以后也不主动让你们排到最后去了,如果你们觉得自己会犹豫不决,就自己主动点直接点一个菜就站后面去,等下次排到的时候也差不多想好吃啥了。这个方法果然有效,大家点了菜后想的第一件事情不是下一个菜吃啥,而是自己会不会犹豫,如果会犹豫那直接排到队伍后面去,如果不会的话就直接接着点菜就行了。这样一来整个队伍没有任何时间是浪费的,效率自然就高了。
这个例子里的排队阿姨的那声吼就是我们的CPU中断,用于切换上下文。每个打饭的学生就是一个task。而每个人自己决定自己要不要让出窗口的这种行为,其实就是我们协程的核心思想。
在用线程的时候,其实虽然CPU把时间给了你,你也不一定有活干,比如你要等IO、等信号啥的,这些时间CPU给了你你也没用呀。
在用协程的时候,CPU就不来分配时间了,时间由你们自己决定,你觉得干这件事情很耗时,要等IO啥的,你就干一会歇一会,等到等IO的时候就主动让出CPU,让别人上去干活,别人也是讲道理的,干一会也会把时间让给你。协程就是使用了这种思想,让编程者控制各个任务的运行顺序,从而最大可能的发挥CPU的性能。
这个例子里的排队阿姨的那声吼就是我们的CPU中断,用于切换上下文。每个打饭的学生就是一个task。而每个人自己决定自己要不要让出窗口的这种行为,其实就是我们协程的核心思想。
在用线程的时候,其实虽然CPU把时间给了你,你也不一定有活干,比如你要等IO、等信号啥的,这些时间CPU给了你你也没用呀。
在用协程的时候,CPU就不来分配时间了,时间由你们自己决定,你觉得干这件事情很耗时,要等IO啥的,你就干一会歇一会,等到等IO的时候就主动让出CPU,让别人上去干活,别人也是讲道理的,干一会也会把时间让给你。协程就是使用了这种思想,让编程者控制各个任务的运行顺序,从而最大可能的发挥CPU的性能。
协程的优点
协程。协程有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
优势一:最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
优势二:就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
使用yield实现协程
import time
# 定义一个消费者
def consumer(name):
'''
这是一个生成器,调用的时候才执行
:param name: 消费者的名字
:return: 返回的是包子
'''
print('消费者准备吃包子------------')
while True:
new_baozi = yield #接收调用生成器的值
print('%s吃第%d个包子'%(name,new_baozi))
time.sleep(1) # 休息一秒
# 定义生产者
def producer(name):
con1.__next__()
con2.__next__()
count = 1
while True:
print('%s正在生产%d个包子和第%d个包子'%(name,count,count+1))
# 调用生成器并且发送数据
con1.send(count)
con2.send(count+1)
count+=2
if __name__ == '__main__':
con1 = consumer('翠花') # 消费者1
con2 = consumer('王大锤') # 消费者2
p = producer('大厨')
为什么yield可以实现协程
在Python中,协程通过yield实现。因为当一个函数中有yield存在的时候,这个函数是生成器,那么当你调用这个函数的时候,你在函数体中写的代码并没有被执行,而是只返回了一个生成器对象,这个需要特别注意。然后,你的代码将会在每次使用这个生成器的时候被执行。
前面讲过yield表达式的两个关键作用:①返回一个值、②接收调用者的参数
“调用者”与“被调用者”之间的通信是通过send()进行联系的
正是因为yield实现的生成器具备“中断等待的功能”,才使得yield可以实现协程。
greenlet 模块
greenlet是一个用C实现的协程模块,相比与python自带的yield,它可以使你在任意函数之
间随意切换,而不需把这个函数先声明为generator
from greenlet import greenlet
def work1():
print(12)
# 3.切换到test2()中执行
gr2.switch()
print(34)
# 又一次切换到test2()中
gr2.switch()
def work2():
print(56)
gr1.switch()
print(78)
# 1、将要执行的函数封装到greenlet对象中
gr1 = greenlet(work1)
gr2 = greenlet(work2)
# 2、想先执行那个函数就可以 对象.switch()方法进行执行
gr1.switch()
gevent模块
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
gevent是第三方库,通过 greenlet 实现 coroutine,创建、调度的开销比 线程(thread) 还小,因此程序内部的 执行流 效率高。
gevent 实现了 python 标准库中一些阻塞库的非阻塞版本,如 socket、os、select 等 (全部的可参考 gevent1.0 的 monkey.py 源码),可用这些非阻塞的库替代 python 标准库中的阻塞的库。
gevent 提供的 API 与 python 标准库中的用法和名称类似。
其基本思想是:当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
gevent特点
基于libev的快速事件循环(Linux上epoll,FreeBSD上kqueue)。
基于greenlet的轻量级执行单元。
API的概念和Python标准库一致(如事件,队列)。
可以配合socket,ssl模块使用。
能够使用标准库和第三方模块创建标准的阻塞套接字(gevent.monkey)。
默认通过线程池进行DNS查询,也可通过c-are(通过GEVENT_RESOLVER=ares环境变量开启)。
TCP/UDP/HTTP服务器
子进程支持(通过gevent.subprocess)
线程池
gevent常用方法:
gevent.spawn() | 创建一个普通的Greenlet对象并切换 |
---|---|
gevent.spawn_later(seconds=3) | 延时创建一个普通的Greenlet对象并切换 |
gevent.spawn_raw() | 创建的协程对象属于一个组 |
gevent.getcurrent() | 返回当前正在执行的greenlet |
gevent.joinall(jobs) | 将协程任务添加到事件循环,接收一个任务列表 |
gevent.wait() | 可以替代join函数等待循环结束,也可以传入协程对象列表 |
gevent.kill() | 杀死一个协程 |
gevent.killall() | 杀死一个协程列表里的所有协程 |
monkey.patch_all() | 非常重要,会自动将python的一些标准模块替换成gevent框架 |
import time
import requests
import gevent
# 使用gevent模块和普通下载进行对比
def f(url):
print('GET:%s'%url)
resp = requests.get(url)
data = resp.text
print('%d bytes received from %s'%(len(data),url))
# 1.普通模式下载
s = time.time()
f('http://www.langlang2017.com./img/banner1.png')
f('http://www.langlang2017.com./img/banner2.png')
f('http://www.langlang2017.com./img/banner3.png')
f('http://www.langlang2017.com./img/banner4.png')
e = time.time()
print('普通模式时间',e-s)
# 2、使用gevent模块
srart = time.time()
gevent.joinall(
[gevent.spawn(f,'http://www.langlang2017.com./img/banner1.png') ,# 创建一个普通的greenlent对象并切换
gevent.spawn(f,'http://www.langlang2017.com./img/banner2.png'),
gevent.spawn(f,'http://www.langlang2017.com./img/banner3.png'),
gevent.spawn(f,'http://www.langlang2017.com./img/banner4.png')]
)
print('gevent时间',time.time()-srart)
'''
如果下载量小的话,普通模式(串行)的下载可能会比,gevent花费的时间少
如果下载的数据量大的话,gevent性能才能显示出来
'''
gevent.spawn() 创建一个普通的Greenlet对象并切换