第十二篇:多任务之协程

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种办法:
    1. 在这条生产线上多招些工人,一起来做剪子,这样效率是成倍増长,即单进程 多线程方式
    2. 老板发现这条生产线上的工人不是越多越好,因为一条生产线的资源以及材料毕竟有限,所以老板又花了些财力物力购置了另外一条生产线,然后再招些工人这样效率又再一步提高了,即多进程 多线程方式
    3. 老板发现,现在已经有了很多条生产线,并且每条生产线上已经有很多工人了(即程序是多进程的,每个进程中又有多个线程),为了再次提高效率,老板想了个损招,规定:如果某个员工在上班时临时没事或者再等待某些条件(比如等待另一个工人生产完谋道工序 之后他才能再次工作) ,那么这个员工就利用这个时间去做其它的事情,那么也就是说:如果一个线程等待某些条件,可以充分利用这个时间去做其它事情,其实这就是:协程方式

 请仔细理解如下的通俗描述

  总结: 

  1. 进程是资源分配的基本单位
  2. 进程是操作系统调度的基本的单位
  3. 进程切换需要的资源最大,效率最大,但是稳定性好,因为一个进程的结束不会影响另外一个进程 ---multiprocessing
  4. 线程切换需要的资源其次,效率一般,稳定性不如进程,切换效率不如协程 --threading
  5. 协程切换任务资源最小,切换效率高,当任务数量大,需要大量切换任务,协程更方便 --gevent
  6. 多进程、多线程可能实现并行,但是协程实在一个线程中运行的,一定为并发

 

 

 

 

 









以上是关于第十二篇:多任务之协程的主要内容,如果未能解决你的问题,请参考以下文章

第十二篇:线程和进程

Python多任务实现 之协程并发下载多图片

第十二篇:编程范式

Python爬虫之协程,异步协程和多任务异步协程

Python异步IO之协程:从yield from到async的使用

Python asyncio之协程学习总结