yhw-miracle

协程

痛点就是起点 writed in

本文为痛点就是起点原创文章,可以随意转载,但需注明出处。

协程(coroutine),又称微线程,纤程,是一种用户级的轻量级线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其他协程共享全局数据和其他资源。

协程需要用户自己编写调度逻辑,对于 CPU 来说,协程其实是单线程,CPU 不需要考虑怎么去调度、切换上下文,这样就省去了 CPU 的切换开销,因此协程在一定程度上有好于多线程。

python 实现协程

第三方库 gevent 提供了比较完善的协程支持,gevent 是一个基于协程的 python 网络数据库,使用 greenlet 在 libev 事件循环顶部提供了一个有高级别并发性的 API。主要特性有以下几点。

  • 基于 libev 的快速事件循环,Linux 上是 epoll 机制。
  • 基于 greenlet 的轻量级执行单元。
  • API 复用了 python 标准库里的内容。
  • 支持 SSL 的协作式 sockets。
  • 可通过线程池或 c-ares 实现 DNS 查询。
  • 通过 monkey patching 功能使得第三方模块变成协作式。

gevent 对协程的支持,本质上是 greenlet 在实现切换工作。greenlet 工作流程如下:假如进行访问网络的 IO 操作,出现阻塞,greenlet 就显式切换到另一段没有阻塞的代码段执行,直到原先的阻塞状况消失以后,再自动切换回原来的代码段继续处理。因此,greenlet 是一种合理安排的串行方式。

由于 IO 操作非常耗时,经常使程序处于等待状态,有了 greenlet 自动切换协程,保证总有 greenlet 在运行,而不是等待 IO,这就是协程一般比多线程效率高的原因。由于切换在 IO 操作时自动完成,所以 gevent 需要修改 python 自带的一些标准库,将一些常见的阻塞,如 socket、select 等地方实现协程跳转,这一过程在启动时通过 monkey patch 完成。

from gevent import monkey; monkey.patch_all()
import gevent, urllib2


def run_task(url):
    print('visit ---> %s' % url)
    try:
        response = urllib2.urlopen(url)
        data = response.read()
        print('%d bytes received from %s.' % (len(data), url))
    except Exception, e:
        print e


if __name__ == '__main__':
    urls = ['http://github.com', 'https://www.python.org', 'http://痛点就是起点.win']
    greenLets = [gevent.spawn(run_task, url) for url in urls]
    gevent.joinall(greenLets)

以上程序主要使用 gevent 中的 spawn 方法和 joinall 方法。spawn 方法是用来形成协程,joinall 方法是添加这些协程任务,并且启动运行。从结果可以看出,3 个网络操作是并发执行的,运行结束顺序不同,但其实只有一个线程。

协程池的实现

gevent 中还提供了对池的支持。当拥有动态数量的 gevent 需要进行并发管理时,如限制并发数,可以使用池来实现,这在处理大量的网络和 IO 操作时是非常需要的。

import urllib2
from gevent.pool import Pool
from gevent import monkey
monkey.patch_all()


def run_task(url):
    print('visit ---> %s' % url)
    try:
        print('%d bytes received from %s.' % (len(urllib2.urlopen(url).read()), url))
    except Exception, e:
        print(e)


if __name__ == '__main__':
    pool = Pool(2)
    urls = ['http://github.com', 'https://www.python.org', 'http://痛点就是起点.win']
    pool.map(run_task, urls)

运行结果可看出,Pool 对象实现了对协程的并发数量管理,代码中规定协程中的池容量为 2,因此程序是先执行前两个任务,当其中一个任务完成是,才会执行第三个任务,达到了限制并发数的目的。

python 协程
知识总结

欢迎关注,我们一起进行认知迭代!


痛点就是起点

© 2016 - 2020 基于 jekyll | Github Pags | iconfont By yhw-miracle