第十二篇:多任务之协程
Posted littlefivebolg
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第十二篇:多任务之协程相关的知识,希望对你有一定的参考价值。
本篇主要介绍协程相关知识,但是在学习协程之前我们需要对迭代器和生成器做更加深入的了解,随后关于实现协程的方式进行了解,其中关于生成器、greenlet模块、gevent模块(重点),最后便是关于进程、线程、携程的总结。
一、迭代器
关于迭代器已经在前面的文件中进行了介绍,但是分类是放在python的函数进阶中,在这里我们会纠正这个说法,详情请点击>>>
可迭代对象和迭代器对象:
可迭代对象:简单来说,可以被for循环遍历的对象即为可迭代对象,当然也可以说拥有__iter__()方法的对象即为可迭代对象;
迭代器对象:迭代器对象是对可迭代对象进行迭代操作的对象,当然也可以说是同时拥有__iter__()方法和__next__()方法的对象即为迭代器对象;
故迭代器对象一定是可迭代对象,但是可迭代对象不一定是迭代器对象。
接下来我们通过自己实现一个生成可迭代对象的模板:
import time class MyIterable(object): """可迭代的对象""" def __init__(self): self.names = list() def add_name(self,name): self.names.append(name) def __iter__(self): # 可迭代对象当执行iter(obj)时,会自动运行__iter__()方法成为一个迭代器对象,同时需将本身传入。 return MyIterator(self) class MyIterator(object): """迭代器对象""" def __init__(self,obj): # self.obj = obj self.current_num = 0 def __iter__(self): pass def __next__(self): """当调用next()方法,自动触发执行,返回可迭代对象中的值""" if self.current_num < len(self.obj.names): ret = self.obj.names[self.current_num] self.current_num += 1 return ret else: # 当抛出StopIteration异常时,for循环机制便会停止循环 raise StopIteration def main(): myiterable = MyIterable() myiterable.add_name("alex") myiterable.add_name("James") myiterable.add_name("amanda") for name in myiterable: print(name) time.sleep(1) if __name__ == "__main__": main()
通过上述可知:
迭代器对象一定有__iter__()方法,当调用iter(obj)将会返回一个迭代器对象,而通过该迭代器对可迭代对象进行操作,通过next()内置函数触发__next__()方法,返回可迭代对象中的值。
二、生成器
在之前的随笔中我提到了生成器为一种特殊的函数,这里纠正一下:
生成器为一类特殊的迭代器,而迭代器并且一个函数,而是一个具有__iter__()和__next__()方法的对象,同样生成器也具备这些。
而实现生成器的方式有两种:
一、通过列表生成式形成生成器:
gener = ( i*2 for i in range(10)) #通过列表生成式形成 print(next(gener)) print(next(gener)) print(gener) # 输出结果为:<generator object <genexpr> at 0x0000026610BBDF68>
通过打印print(gener)可以看出其为一个生成器对象,那么我们接下来看一下其方法:在终端输入:help(gener),得出:
从输出结果可以看出:生成器对象也有__iter__和__next__()方法,故生成器为一个特殊的迭代器对象。
二、通过在函数中调用yield语句
首先我们来看一个实例,以斐波那契数列来举例:
#用普通函数实现的斐波那契数列 def feibonacil(num): a, b = 0, 1 current_num = 0 num_list = [] while True: num_list.append(a) a, b = b,a+b if current_num > num: break current_num += 1 return num_list ret = feibonacil(10) print(ret) #输出结果为:[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89] #通过生成器实现斐波那契数列 def feibonacil(num): a, b = 0, 1 current_num = 0 num_list = [] while True: yield a a, b = b,a+b if current_num > num: break current_num += 1 ret = feibonacil(10) print(ret) # 输出结果为:<generator object feibonacil at 0x000001C3594FDF68> #通过next()可以不断获取生成器中的值
注:只要在函数中引用了yield语句,则便不再是一个函数,而是一个生成器对象生成模板,当调用时获得返回值,其便是一个生成器对象。
那么生成器是怎样执行的呢?我们可以进行测试一下:
# 测试迭代生成器对象的过程 def feibonacil(num): a, b = 0, 1 current_num = 0 num_list = [] print("---test1----") while True: print("----test2----") yield a print("----test3-----") a, b = b,a+b if current_num > num: break current_num += 1 ret = feibonacil(3) for i in ret: print(i) #生成结果为: ---test1---- ----test2---- 0 ----test3----- ----test2---- 1 ----test3----- ----test2---- 1 ----test3----- ----test2---- 2 ----test3----- ----test2---- 3 ----test3-----
通过测试结果我们可以知道:
当遍历到yield语句时,将会得到一个返回值并且停止在此处保持状态,得到下一次触发执行时,还会接着yield语句后面进行执行,直到遇到yield语句获得返回值,周而复始。
故我们生成器遇到yield便停止运行且保持状态的特性,可以实现协程的效果,即当我们在这个生成器运行遇到yield暂停保持状态,运行下一个生成器直到遇到yield暂停保持状态,如此便能实现并发的效果。接下来看协程是如何实现的。
三、协程
1、协程是什么?
协程是Python中实现多任务的另外的一种方式,但是与线程、进程不同的是,它比线程占用的资源更小,且自带CPU上下文,即只要在合适的时机(阻塞或者优先级更高的),可以从一个协程切换到另外一个协程,而只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。
在简单了解协程之后,我们来了解几种实现协程的方式:
一、协程 - yield
#!/usr/bin/env python # -*- coding:utf-8 -*- import time def worker1(): """生成器worker1""" while True: print("---worker1---") yield time.sleep(1) def worker2(): """生成器worker2""" while True: print("---worker2---") yield time.sleep(1) w1 = worker1() w2 = worker2() # 通过循环next()轮流触发生成器对象,实现并发的效果 for i in range(10): next(w1) next(w2)
从上述案例中可以看出:由于生成器每次调用__next__()方法,均会执行到yield语句暂停保持状态,这样就可以切换两个函数使其来回执行,通过不断循环实现并发多任务的效果。
二、协程 - greenlet
#!/usr/bin/env python # -*- coding:utf-8 -*- #coding=utf-8 from greenlet import greenlet #导入greenlet模块 import time def test1(): print("---A--") gr2.switch() time.sleep(0.5) print("--C--") def test2(): print("---B--") gr1.switch() time.sleep(0.5) print("--D--") gr1 = greenlet(test1) gr2 = greenlet(test2) #切换到gr1中运行 gr1.switch() #执行结果: ---A-- ---B-- --C--
解析:这里创建了两个greenlet协程对象,gr1和gr2,分别对应于函数test1()和test2()。使用greenlet对象的switch()方法,即可以切换协程。上例中,我们先调用”gr1.switch()”,函数test1()被执行,然后打印出”--A--″;接着由于”gr2.switch()”被调用,协程切换到函数test2(),打印出”--B--″;之后”gr1.switch()”又被调用,所以又切换到函数test1()。但注意,由于之前test1()已经执行到”gr2.switch()”,所以切换回来后会继续往下执行,也就是打印”--C--″;现在函数test1()退出,同时程序退出。由于再没有”gr2.switch()”来切换至函数test2(),所以程序”print("--D--")″不会被执行。
从上面例子中可以看出:协程本质上是串行的 ,无法发挥多核CPU的优势。
三、协程 - gevent
其原理相当于:当程序运行到需要耗时操作或者处于阻塞的转态(比如I/O操作、网络、文件操作等耗时操作)时,通过gevent可以自动从这部分处于等待的转态的协程A,切换到另外一个已经处于就绪状态的协程B中执行,等到合适的时机协程A不再处于等待的转态时,在切换回来继续协程A的运行。
#---------------------gevent-------------------------------- import gevent from gevent import monkey import time #有耗时操作需要 # 将程序中用到的耗时操作的代码,换为gevent中自己实现的模块 monkey.patch_all() def worker1(name): for age in range(20,23): print("worker1[%s] is %s years old."%(name,str(age))) time.sleep(1) def worker2(name): for age in range(30,33): print("worker2[%s] is %s years old."%(name,str(age))) time.sleep(1) g1 = gevent.spawn(worker1,"alex") # 创建协程对象g1,工作函数为worker1(产卵) g2 = gevent.spawn(worker2,"wupeiqi") # 创建协程对象g2,工作函数为worker2 gevent.joinall([g1,g2]) #协程对象g1,g2工作
#执行结果为:
worker1[alex] is 20 years old.
worker2[wupeiqi] is 30 years old.
worker1[alex] is 21 years old.
worker2[wupeiqi] is 31 years old.
worker1[alex] is 22 years old.
worker2[wupeiqi] is 32 years old.
从上述例子中我们可以看出:
通过gevent实现协程的过程和threading实现线程、multiprocessing实现进程的过程十分相似,通过gevent.spawn(work_func,"para1,para2",)创建协程对象,通过gevent.joinall(list(g_obj1,g_obj2))来触发协程对象进行工作,同时需注意的是:当有耗时操作时,需为程序打补丁 -----> monkey.pacth_all()来使其他模块的耗时操作转换为gevent直接实现的模块的耗时操作。
四、线程、进程、协程的区别及应用场景:
- 有一个老板想要开个工厂进行生产某件商品(例如剪子):
- 他需要花一些财力物力制作一条生产线,这个生产线上有很多的器件以及材料这些所有的 为了能够生产剪子而准备的资源称之为:进程
- 只有生产线是不能够进行生产的,所以老板的找个工人来进行生产,这个工人能够利用这些材料最终一步步的将剪子做出来,这个来做事情的工人称之为:线程
- 这个老板为了提高生产率,想到3种办法:
- 在这条生产线上多招些工人,一起来做剪子,这样效率是成倍増长,即单进程 多线程方式
- 老板发现这条生产线上的工人不是越多越好,因为一条生产线的资源以及材料毕竟有限,所以老板又花了些财力物力购置了另外一条生产线,然后再招些工人这样效率又再一步提高了,即多进程 多线程方式
- 老板发现,现在已经有了很多条生产线,并且每条生产线上已经有很多工人了(即程序是多进程的,每个进程中又有多个线程),为了再次提高效率,老板想了个损招,规定:如果某个员工在上班时临时没事或者再等待某些条件(比如等待另一个工人生产完谋道工序 之后他才能再次工作) ,那么这个员工就利用这个时间去做其它的事情,那么也就是说:如果一个线程等待某些条件,可以充分利用这个时间去做其它事情,其实这就是:协程方式
请仔细理解如下的通俗描述
总结:
- 进程是资源分配的基本单位
- 进程是操作系统调度的基本的单位
- 进程切换需要的资源最大,效率最大,但是稳定性好,因为一个进程的结束不会影响另外一个进程 ---multiprocessing
- 线程切换需要的资源其次,效率一般,稳定性不如进程,切换效率不如协程 --threading
- 协程切换任务资源最小,切换效率高,当任务数量大,需要大量切换任务,协程更方便 --gevent
- 多进程、多线程可能实现并行,但是协程实在一个线程中运行的,一定为并发
以上是关于第十二篇:多任务之协程的主要内容,如果未能解决你的问题,请参考以下文章