python协程入门
函数的执行顺序
在了解协程之前, 我们需要再次回想一下python中的多个函数执行的顺序是怎样的?
-
我们看看下面一段代码,在没有在解释器运行之前,你是否知道函数的最后输出的内容呢
#!/usr/bin/python #-*-coding:utf-8-*- def A1(): print("i am the func:{}".format(A1.__name__)) def B1(): A1() print("i am the func:{}".format(B1.__name__)) def C1(): print("i am the func:{}".format(C1.__name__)) B1() print("---all funcs over---") if __name__ == "__main__": C1() #output i am the func:C1 i am the func:A1 i am the func:B1 ---all funcs over---
函数从C1开始,在执行过程中,遇到函数B1此时函数就跳转到函数B1去执行,在跳转到B1后,在其函数内又遇到A1函数,此时cpu又会切换到A1函数所在的内存中执行,当A1执行完毕后,跳转到B1继续执行,B1执行完毕后,才返回到函数C1执行!
内部执行流程图如下
对于正常函数之间的调用:是通过 栈执行的一个子程序,子程序调用总是一个入口,返回之后再继续跳转到另一个函数内部。
协程概念
-
看上去也是子程序,但是在自沉协的内部可以中断, 然后转而执行别的子程序, 但
不是转到别的函数
,它的执行过程像线程的切换,但是只会在一个线程中执行(中断,跳转执行) -
优点(与线程相比):执行效率
极高
,因为只有一个线程, 不存在同时写同一个变量的冲突,在协程中共享资源不用加锁(多线程在执行对同一个数据写操作时为了避免冲突必须加锁),只需要判断状态即可 -
如何实现: 通过生成器实现,函数中通过关键字
yield
实现,控制函数内部的阶段性执行,返回值的时一个生成器。
一个简单的协程
-
如何调用
#!/usr/bin/python #-*-coding:utf-8-*- def A1(): print(1) yield 10 print(2) yield 20 if __name__ == "__main__": m= A1() try: print(type(m)) print(next(m)) print(next(m)) print(next(m)) except StopIteration: print("over") #output <class 'generator'> 1 10 2 20 over
执行A1,输出的是<class ’generator’>,可以看出,函数使用yield关键字之后,类型变成了一个生成器, 调用的时候使用next来执行,
- 第一次通过next执行遇到
yield
之后,会中断执行后面的代码 - 第二次通过next执行时,cpu会读取上次执行的位置,再次执行直到遇到yield执行完毕后再次停止
- 当检测到没有yield时,生成器再也没有更多的数据时,会发生
StopIteration
具有生成器的所有的特性。
- 第一次通过next执行遇到
协程间的数据传输
协程是单个函数(一个线程),可以随时中断执行,也就意味着,在中断过程中,可以做一些有意义的事情(它并不像普通函数间的调用,一个函数在执行后是没办法继续去操作该函数的,如传递新的数据,修改函数内部的变量等),比如可以在此时传递新的数据!
-
由于协程是
函数
及生成器
的综合体,so,它拥有了两者的共同特性- 可以携带参数
- 可以有返回值
- 可以使用for循环调用
- 可以使用
send
方法
-
看这个列子,注意理解协程函数是通过什么样的方式在执行过程中传递外部数据的
#!/usr/bin/python #-*-coding:utf-8-*- def run(): # 空变量,存储的作用, data始终为空 data = "" r = yield data # 返回数据,r用来接收从函数外面传递的数据 print(1, r, data) r = yield data print(2, r, data) yield 20 if __name__ == "__main__": m = run() # 启动协程 m.send(None) m.send("a") m.send("b") print("*****") #output 1 a 2 b *****
代码解释
-
先看入口:
m=run()
想象一下,如果run函数中yield
是关键字return
, 那么此时函数返回的是一个变量,但是此时使用yield
,那么一切都变了, 它不再是一个函数,而是一个生成器。 -
m 是一个生成器, 所以它有生成器的特性,send是它的方法,使用
m.send(None)
来启动协程 -
m.send("a")
, 此时将“a”的值传递进生成器内部,那么内部如何接受这个外来数据呢
?于是我们用r
变量来接收 -
send方法到底执行了哪些操作呢?实际上是包含来两个步骤
- 发送数据到生成器内部
- 执行
next()
方法直到遇到下一个yield暂停执行
-
data 始终为
空字符串
,因为 r 其实与 data没有任何关系,在run函数未执行完毕之前,data的值没有发生任何改变 -
line 14, send执行是遇到下一个yield才会终止,所以必须使用yield终止
-
协程之生产者消费者模型
-
什么是生产者与消费者模型?
- 在现实生活中:
生产者
随处可见,顾名思义可以创建某些产品的机构或个人
,消费者则是用来消化某些产品的用户或者机构
- 在计算机的世界:这是一种设计模型,我们都知道,不管是小到简单的一组数据的加减,还是web工程项目,本质都是
cpu
内部执行的一段代码,代码运行在计算机内部(内存中)都是读写操作,生产者消费者模型
中,生产者
用来产生程序内部需要的数据,消费者
则用来处理这些数据!
- 在现实生活中:
-
之前我们使用多线程与消息队列实现来消费者与生产者模型,那么协程是否也能实现此种设计模型呢?
- 协程可以从外部传递数据的特性
- 可以随时中断执行
-
生产者消费者
#!/usr/bin/python #-*-coding:utf-8-*- '''协程实现生产者与消费者模型''' def product(c): '''生产者:厨师生产包子 - 生产者接收消费者发来的消息. 以 c.send(None)触发生成器函数 ''' c.send(None) for i in range(100000): if i == 0: print("没包子了,厨师正在赶做...") else: print("厨师包好了第{}个包子".format(i)) c.send(i) print("顾客吃完了{}个包子".format(i)) print("今天限量包子已卖完") c.close() def customer(): '''消费者:顾客吃包子''' data = "" # 定义一个常量用来为生成器占用内存 while True: n = yield data # n 用来接收c.send发送来的数据 if not n: # 判断n 是否接收到了数据,如果没有接收到,实际n = data print("已经没包子了,稍等...") continue print("顾客正在吃第{}个包子".format(n)) if __name__ == "__main__": c = customer() product(c)
总结
- 协程的实现:
yield
关键字 - 协程实际上是 :
生成器函数
- 使用
g.send(None)
触发协程 g.send("a")
像xie程内部发送数据g.close()
关闭协程