Python协程讲解

上篇文章我们说过由于GIL锁的限制,导致Python不能充分利用多线程来实现高并发,在某些情况下使用多线程可能比单线程效率更低,所以Python中出现了协程
协程(coroutine) 又称微线程,是一中轻量级的线程,它可以在函数的特定位置暂停或恢复,同时调用者可以从协程中获取状态或将状态传递给协程。进程和线程都是通过CPU的调度实现不同任务的有序执行,而协程是由用户程序自己控制调度的,也没有线程切换的开销,所以执行效率极高。

生成器方式实现

早先的协程是使用生成器关键字yield来实现的,和生成器很相似。下面利用一生产者-消费者模型来介绍如何使用yield协程来进行任务的切换。


 

import time


def consumer():
    r = None
    while True:
        n = yield r
        if not n:
            return
        print("[消费者] 消费'{}'...".format(n))
        time.sleep(1)
        r = '消费完成'


def producer(c):
    next(c)
    n = 0
    while n < 5:
        n = n + 1
        print("[生产者] 生产'{}'...".format(n))
        res = c.send(n)
        print("[生产者] 消费者返回'{}'".format(res))
    c.close()


if __name__ == '__main__':
    c = consumer()
    producer(c)

执行结果如下:

Python协程讲解_第1张图片

上述代码中的consumer函数是一个生成器,

  • 把consumer传入producer函数,调用next(c)启动该生成器;
  • 生产者producer函数生产了n,通过c.send(n)切换到consumer执行;
  • consumer函数通过yield拿到生产者生产的消息n,处理后,又通过yield把结果r返回。
  • producer拿到consumer处理的结果,继续生产下一条消息,直至生产结束,通过c.close()关闭consumer,整个过程结束。

这段代码全程在一个线程中完成,producer和consumer协作完成任务,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高。

greenlet

如果有上百个任务,要想实现在多个任务之间切换,使用yield生成器的方式就过于麻烦,而greenlet模块可以很轻易的实现。
Greenlet是Python的⼀个C扩展,旨在提供可⾃⾏调度的"微线程",也就是协程。在greenlet模块中,通过target.switch()可以切换到指定的协程,可以更简单的进行切换任务。

可以使用命令pip install greenlet安装greenlet模块。

from greenlet import greenlet
import time


def work1():
    while True:
        print("work1开始执行...")
        g2.switch()  # 切换到g2中运行
        time.sleep(0.5)


def work2():
    while True:
        print("work2开始执行...")
        g1.switch()  # 切换到g1中运行
        time.sleep(0.5)


if __name__ == "__main__":
    # 定义greenlet对象
    g1 = greenlet(work1)
    g2 = greenlet(work2)

    g1.switch()  # 切换到g1中运行

执行结果就是work1和work2交替执行。

gevent

虽然greenlet模块实现了协程并且可以方便的切换任务,但是仍需要人工切换,而不是自动进行任务的切换,当一个任务执行时如果遇到IO(⽐如⽹络、⽂件操作等),就会阻塞,没有解决遇到IO自动切换来提升效率的问题。
其实Python还有⼀个⽐greenlet更强⼤的协程模块gevent,gevent也是基于greenlet的,可以实现任务的自动切换,当⼀个greenlet遇到IO操作时,就会⾃动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执⾏。这样就跳过了IO操作的时间,⽽不是等待IO完成,可以提升程序的运行效率。

可以使用命令pip install gevent安装gevent模块。


 

import gevent
import time


def work1():
    for i in range(5):
        print("work1开始执行...", gevent.getcurrent())
        time.sleep(0.5)


def work2():
    for i in range(5):
        print("work2开始执行...", gevent.getcurrent())
        time.sleep(0.5)


if __name__ == "__main__":
    g1 = gevent.spawn(work1)
    g2 = gevent.spawn(work2)
    # 等待协程执⾏完成再关闭主线程
    g1.join()
    g2.join()

执行结果如下:

Python协程讲解_第2张图片

我们希望的是gevent模块帮我们⾃动切换协程,以达到work1和work2交替执⾏的⽬的,但并没有达到效果,原因是因为我们使用time.sleep(0.5)来模拟IO耗时操作,但是这样并没有被gevent正确识别为IO操作,所以要使⽤下⾯的gvent.sleep()来实现耗时

import gevent
import time


def work1():
    for i in range(5):
        print("work1开始执行...", gevent.getcurrent())
        gevent.sleep(0.5)


def work2():
    for i in range(5):
        print("work2开始执行...", gevent.getcurrent())
        gevent.sleep(0.5)


if __name__ == "__main__":
    g1 = gevent.spawn(work1)
    g2 = gevent.spawn(work2)
    # 等待协程执⾏完成再关闭主线程
    g1.join()
    g2.join()

运行结果如下:

Python协程讲解_第3张图片

猴子补丁

上面把time.sleep()改写成gevent.sleep()后,work1和work2能够交替执⾏,那么有没有不用改写,就可以实现的方法吗?答案是有的,那就是给程序打猴子补丁

 

关于猴⼦补丁:这个叫法起源于Zope框架,⼤家在修正Zope的Bug的时候经常在程序后⾯追加更新部分,这些被称作是"杂牌军补丁"(guerilla patch),后来guerilla就渐渐的写成了gorllia(猩猩),再后来就写成了monkey(猴⼦),所以现在被称为猴⼦补丁。

猴⼦补丁主要有以下⼏个⽤处:

  • 在运⾏时替换⽅法、属性等
  • 在不修改第三⽅代码的情况下增加原来不⽀持的功能
  • 在运⾏时为内存中的对象增加patch⽽不是在磁盘的源代码中增加

可以使用以下代码给程序打猴子补丁:


 

import gevent

# 打补丁,让gevent识别⾃⼰提供或者⽹络请求的耗时操作
from gevent import monkey
monkey.patch_all()

import time


def work1():
    for i in range(5):
        print("work1开始执行...", gevent.getcurrent())
        time.sleep(0.5)


def work2():
    for i in range(5):
        print("work2开始执行...", gevent.getcurrent())
        time.sleep(0.5)


if __name__ == "__main__":
    g1 = gevent.spawn(work1)
    g2 = gevent.spawn(work2)
    # 等待协程执⾏完成再关闭主线程
    g1.join()
    g2.join()

执行结果如下:

Python协程讲解_第4张图片

可以看出给程序打上猴子补丁后,使用time.sleep(),gevent也能识别到,可以自动切换任务。

async/await异步协程

在python2以及python3.3之前,使用协程要基于greenlet或者gevent这种第三方库来实现,由于不是Python原生封装的,使用起来可能会有一些性能上的流失。但是在python3.4中,引入了标准库asyncio,直接内置了对异步IO的支持,可以很好的支持协程。可以使用asyncio库提供的@asyncio.coroutine把一个生成器函数标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。

而后为了简化并更好地标识异步IO,从Python3.5开始引入了新的语法async和await,把asyncio库的@asyncio.coroutine替换为async,把yield from替换为await,可以让coroutine的代码更简洁易读。

其中async关键字用来声明一个函数为异步函数,异步函数的特点是能在函数执行过程中挂起,去执行其他异步函数,等到挂起条件消失后再回来继续执行。

await关键字用来实现任务挂起操作,比如某一异步任务执行到某一步时需要较长时间的耗时操作,就将此挂起,去执行其他的异步程序。注意:await后面只能跟异步程序或有__await__属性的对象。

假设有两个异步函数async work1和async work2,work1中的某一步有await,当程序碰到关键字await work2()后,异步程序挂起后去执行另一个异步work2函数,当挂起条件消失后,不管work2是否执行完毕,都要马上从work2函数中回到原work1函数中继续执行原来的操作

import asyncio
import datetime


async def work1(i):
    print("work1'{}'执行中......".format(i))
    res = await work2(i)
    print("work1'{}'执行完成......".format(i), datetime.datetime.now())
    print("接收来自work2'{}'的:", res)


async def work2(i):
    print("work2'{}'执行中......".format(i))
    await asyncio.sleep(1.5)
    print("work2'{}'执行完成......".format(i), datetime.datetime.now())
    return "work2'{}'返回".format(i)


loop = asyncio.get_event_loop()  # 创建事件循环
task = [asyncio.ensure_future(work1(i)) for i in range(5)]  # 创建一个task列表
time1 = datetime.datetime.now()
# 将任务注册到事件循环中
loop.run_until_complete(asyncio.wait(task))
time2 = datetime.datetime.now()
print("总耗时:", time2 - time1)
loop.close()

运行结果如下:

Python协程讲解_第5张图片

从結果可以看出,所有任务差不多是在同一时间执行结束的,所以总耗时为1.5s,证明程序是异步执行的。

总结

至此,Python中协程的用法已经总结完毕。在平时开发中,写协程用的最多的还是async/await或者第三方库gevent模块,yield的方式只用于写生成器。关于异步协程,Python的支持和封装也越来越完善,用起来语法也很简单,可是如果我们要透过语法去学习他们优秀的思想,却不是一件容易的事情。


看到最后喜欢的话别忘了点个赞哈!

 

你可能感兴趣的:(Python,python,开发语言,程序人生,编程语言,爬虫)