什么是协程
协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程。
协程的特点
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
协程的优点
- 无需线程上下文切换的开销
- 无需原子操作锁定及同步的开销
"原子操作(atomic operation)是不需要synchronized",所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。(因为携程只有一个线程)
- 方便切换控制流,简化编程模型
- 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。(一个进程一个携程,而携程又可以开多个携程,CPU一个核心开一个进程)
协程的缺点
- 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
- 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序 (携程类似以一个管道,如果一个地方堵住了,那么整个就不通了)
最底层的协程
使用yield实现协程操作例子
一个服务端在创建,而两个客户端在接收。在单线程上几乎同时执行,代码如下:
#客户的方法
def customer(name):
print("===准备买东西===")
while True:
ret = yield
print("名字:%s,买了:%s"%(name,ret)) #服务端
def produce(num):
next(cus1)
next(cus2)
values = 0
while values < num:
values +=1
cus1.send(values)
cus2.send(values) print("谢谢惠顾") if __name__ == "__main__":
cus1 = customer("小明") #创建2个客户端方法的对象
cus2 = customer("小白") produce(5)
#运用生成器的,yield,send,next的特性就可以弄一个最简单的协程
Python内部来实现协程的模块gevent
gevent模块不属于Python标准库的模块,要的话得自己下载。下面就让我们看看怎么下载。
我们也可以用greenlet模块实现的协程
greenlet模块也是刚刚在下载gevent模块才有的模块。from greenlet import greenlet ,创建协程对象的方法其实有2个参数“greenlet(run=None,parent = None)”。参数“run”就是要调用的方法,比如下例子中的t1,t2,参数“parent”定义了该协程的父协程,也就是说,greenlet协程直接是可以有父子关系的。如果不设或设为空,则其父协程就是程序默认的“main”主协程。而在要调用的方法,如下面的t1对象中,有一个重要的方法t1.switch(),这个方法的意思就是暂停当前的方法,切换到t1。
实例如下代码
from greenlet import greenlet #调用模块中的方法 def fun1():
print('a')
t2.switch()
print('b') def fun2():
print('c')
t1.switch()
print('d') t1 = greenlet(fun1) #创建greenlet对象
t2 = greenlet(fun2) t1.switch() #执行t1 #我们可以看到只执行了acb,d并没有执行,因为fun1后面还少了个switch()
#所以可以看出switch还有暂停作用
Gevent下所支持的协程
在讲这个之前引进一个内容
猴子补丁 Monkey patching
了实现网络通信的高并发,但是,Gevent直接修改标准库里面大部分的阻塞式系统调用,包括socket、ssl、threading和 select等模块,而变为协作式运行。但是我们无法保证你在复杂的生产环境中有哪些地方使用这些标准库会由于打了补丁而出现奇怪的问题。
一种方法是使用gevent下的socket模块,我们可以通过”from gevent import socket”来导入。不过更常用的方法是使用猴子布丁(Monkey patching)。使用猴子补丁褒贬不一,但是官网上还是建议使用”patch_all()”,而且在程序的第一行就执行。猴子补丁也在Gevent模块的方法中。
Gevent 实现了异步I/O的协程,也就是说只有在遇到I/O阻塞的时候才能切换到其他的方法,而这个I/O阻塞即是可以是执行代码的偶然,也可以是我们定义的必然。
import gevent 这个gevent模块
在看代码之前要知道gevent方法这个对象中的几个方法:
spawn(func,func_args) func:加入要实现gevent的方法的名字,func_args:方法要传入的参数 创建协程实例
joinall([spawn_list,]) spawn_list:添加入我们刚刚创建的gevent对象 类似于一个同时启动
sleep(time) time:时间(秒) 交出CPU控制权限,时间为秒,类似于一个I/O阻塞。这里不能用time.sleep代替
getcurrent() 获取当前协程内存地址
下面就让我们来看看例子:
from gevent import monkey #引入monkey方法 import gevent,time monkey.patch_all() #这个在小的协程中体现不出效果,不过建议加下。养成个习惯 def a():
print("1")
gevent.sleep(2) #模拟I/O阻塞2秒
print("2") def b():
print("3")
gevent.sleep(1)
print("4") start = time.time()
gevent.joinall([ #这里必须是个列表
gevent.spawn(a),
gevent.spawn(b),
]
) print(time.time()-start)
#这里可以看到实际上才执行了2秒多一点