Python 协程:yield,greenlet,gevent,asyncio

进程

进程是资源分配的最小单位,拥有独立的内存空间,有寄存器信息、堆、栈、数据段、代码段、虚拟内存、文件句柄、IO 状态、信号信息等等内容,不同进程的切换开销比较大,同时进程比较独立稳定,通常不受其他进程影响

进程间的通信有管道(Pipe)、消息队列(Message Queue)、信号量(Semaphore)、共享内存(Shared Memory)、套接字(Socket)等等

线程

线程是系统调度的最小单位,只需要保存自己的栈、寄存器信息等少量内容,一个进程至少要有一个线程,不同线程的切换开销比进程切换要小很多,但线程不够独立稳定,容易受进程和其他线程的影响

由于不同线程都是共享同一段内存,线程间通信直接使用共享内存,就是使用全局定义的变量即可,另外不同的线程间通常还需要通过锁实现同步、互斥等功能

协程

进程和线程都是操作系统调度的,虽然线程切换开销比进程要小,但如果是频繁切换,依然会严重影响性能

操作系统通常在三种情况下会进行切换

  1. 程序运行时间比较长
  2. 有更高优先级的程序抢占
  3. 程序发生了阻塞

在很多网络应用中,会同时接受大量请求,这些请求计算量很小,主要的时间是耗在 IO 上了,并且最主要是网络的 IO 时间,导致了频繁的 IO 阻塞和线程切换,严重影响性能

协程就是为了解决以 IO 为主要开销的程序,在高并发场景下的性能问题

在一个线程内可以运行多个协程,当一个协程调用了需要 IO 阻塞的命令时,会使用异步 IO 的方式,避免触发操作系统进行切换,然后继续执行另一个协程,由于是在同一个线程内实现,切换开销非常小,性能会有很大提升

注意协程在 IO 并发量很大的情况下作用才比较明显,因为只有这种情况下才能保证随时有异步 IO 准备就绪可以执行的,如果 IO 量很小,比如 10 分钟才有一条请求,那做了异步操作后,还是得等待这个异步 IO 就绪,照样会导致线程切换

注意协程只在一种情况下会切换:IO 调用

这个功能需要由程序框架实现,对操作系统是透明的,对应用程序也是透明的,这样既避免了以 IO 为主要开销的程序在高并发时频繁地触发多线程的切换,又不增加应用程序开发的工作量

在 Go 语言中,这个功能是原生的,Go 语言本身就实现了这个功能,在语法层面上就支持
在 Python 语言中,这个功能由 gevent 包提供支持

下面主要讲 Python 的协程

yield

yield 是为了生成器使用的,比如下面的代码

def f(max): 
    n = 1
    while n <= max: 
        yield n*n
        n = n + 1
        
for i in f(5):
    print(i)

如果不使用 yield,那么函数 f 就需要返回一个 list,如果 max 非常大,那么就需要创建一个很大的内存在放这个 list,而在使用了 yield 后,函数被当成迭代器,f(5) 返回的是一个迭代器,for 语句每次取值的时候触发迭代器,迭代器执行到 yield 命令时返回 n*n 并停止执行,直到 for 下一次取值,迭代器再从 n = n + 1 继续执行,这样无论 max 多大,内存的使用都是恒定的

再举一个例子

def f():
    n = 1
    print("f function with yield inside")
    while True:
        msg = yield n
        print("msg: ", msg)
        n = n + 1

iter = f()
print("before invoke next")
print("receive: ", next(iter))
print("after invoke next")
print("receive: ", next(iter))

返回的是

before invoke next
f function with yield inside
('receive: ', 1)
after invoke next
('msg: ', None)
('receive: ', 2)

可以看到调用 iter = f() 的时候没有打印任何信息出来,即 f() 函数其实没有被执行,而是返回了一个迭代器,当执行 next(iter) 函数时 (next 是 python 内置函数),f() 函数才被执行,并且这里只执行到 yield n 就停止继续执行并将 n 作为结果返回 (这里连 msg 的赋值都没执行,后面会进一步讲到),等下一个 next 函数时,会从 msg 的赋值开始继续执行,直到再次遇见 yield,如果迭代器已经执行完,那么 next 函数会报 StopIteration 异常

继续下一个例子

def f():
    n = 1
    print("f function with yield inside")
    while True:
        msg = yield n
        print("msg: ", msg)
        n = n + 1

iter = f()
print("before invoke next")
print("receive: ", next(iter))
print("after invoke next")
print("receive: ", iter.send("from outside"))

这里把第二个 next 换成调用迭代器的 send 函数
返回的是

before invoke next
f function with yield inside
('receive: ', 1)
after invoke next
('msg: ', 'from outside')
('receive: ', 2)

和上一个例子的唯一区别就是打印的 msg 不是 None 而是 send 函数的参数,send 函数和 next 一样会触发迭代器继续执行,但同时会将参数作为 yield 语句的结果赋值给 msg

下面用 yield 模拟协程

def f_0():
    n = 5
    while n >= 0:
        print('[f_0] ' + str(n))
        yield
        n = n - 1

def f_1():
    m = 3
    while m >= 0:
        print('[f_1] ' + str(m))
        yield
        m = m - 1

iter_list = [f_0(), f_1()]
while True:
    for it in iter_list:
        try:
            next(it)
        except:
            iter_list.remove(it)

        if len(iter_list) == 0:
            break

返回结果为

[f_0] 5
[f_1] 3
[f_0] 4
[f_1] 2
[f_0] 3
[f_1] 1
[f_0] 2
[f_1] 0
[f_0] 1
[f_0] 0

可以看到实现了两个函数不断切换的功能,但代码写起来麻烦点

greenlet

greenlet 是底层实现了原生协程的 C 扩展库

from greenlet import greenlet

def f_0():
    n = 5
    while n >= 0:
        print('[f_0] ' + str(n))
        parent_greenlet.switch()
        n = n - 1

def f_1():
    m = 3
    while m >= 0:
        print('[f_1] ' + str(m))
        parent_greenlet.switch()
        m = m - 1

def parent():
    while True:
        for task in greenlet_list:
            task.switch()
            if task.dead:
                greenlet_list.remove(task)
        if len(greenlet_list) == 0:
            break

parent_greenlet = greenlet(parent)
greenlet_list = [greenlet(f_0, parent_greenlet), greenlet(f_1, parent_greenlet)]
parent_greenlet.switch()

返回

[f_0] 5
[f_1] 3
[f_0] 4
[f_1] 2
[f_0] 3
[f_1] 1
[f_0] 2
[f_1] 0
[f_0] 1
[f_0] 0

switch 也可以传值,根据程序运行情况会传给函数参数,或是传给 switch 的返回

def test1(x, y):
    z = gr2.switch(x+y)
    print(z)

def test2(u):
    print(u)
    gr1.switch(42)
    print "end"

gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch("hello", " world")

返回

hello world
42

可以看到没有打印出 end,因为没指定 parent,默认有一个结束就返回 main,另一个就不会执行了,如果有指定 parent,则结束后会返回 parent

gevent

greenlet 写起来也比较复杂,并且 greenlet 只实现了协程,却没有实现捕获 IO 操作并进行切换的功能,实际上一般的计算并不需要协程的切换,性能没什么影响,只有在高并发 IO 操作时能切换程序,其性能才会有较大提升

gevent 基于 greenlet,使用了包括 linux 的 epoll 事件监听机制在内的许多优化措施,以提升高并发 IO 的性能,比如当一个 greenlet 程序需要做网络 IO 操作时,就将其注册为异步监听,并切换到其他 greenlet 程序,等 IO 完成,在适当的时候会再切回来继续执行,这样当 IO 很高时,可以让程序一直在运行,而不是把时间耗在 IO 等待上,同时又能避免线程的切换开销

import gevent

def f_0(param):
    n = param
    while n >= 0:
        print('[f_0] ' + str(n))
        gevent.sleep(0.1)
        n = n - 1

def f_1(param):
    m = param
    while m >= 0:
        print('[f_1] ' + str(m))
        gevent.sleep(0.1)
        m = m - 1

g1 = gevent.spawn(f_0, 5)
g2 = gevent.spawn(f_1, 3)
gevent.joinall([g1, g2])

返回

[f_0] 5
[f_1] 3
[f_0] 4
[f_1] 2
[f_0] 3
[f_1] 1
[f_0] 2
[f_1] 0
[f_0] 1
[f_0] 0

可以看到代码很简洁清楚,和正常程序相比,就是用 gevent.sleep() 替换了 time.sleep() 是 gevent 能在需要阻塞的地方做协程的切换

实际上还可以更简单

import time
import gevent

from gevent import monkey

monkey.patch_all()

def f_0(param):
    n = param
    while n >= 0:
        print('[f_0] ' + str(n))
        time.sleep(0.1)
        n = n - 1

def f_1(param):
    m = param
    while m >= 0:
        print('[f_1] ' + str(m))
        time.sleep(0.1)
        m = m - 1

g1 = gevent.spawn(f_0, 5)
g2 = gevent.spawn(f_1, 3)
gevent.joinall([g1, g2])

通过 monkey.patch_all() 打补丁,可以拦截到大量 IO 操作,比如 time sleep,http request 等,对其做异步执行,并切换协程,这种做法的最大的好处是原函数不用修改就能直接使用,对程序开发人员而言,协程就是透明的,不用特意修改代码,交给 gevent 打理就可以

asyncio

Python 3.6 中正式引入了 asyncio 库作为 python 标准库

最主要是 async 和 await 关键字

async 用来声明一个函数为异步函数,可以被挂起

await 用来用来声明程序被挂起,await 后面只能跟异步程序或有 __await__ 属性的对象

import asyncio
import aiohttp

async def f_0(param):
    n = param
    while n >= 0:
        print('[f_0] ' + str(n))
        await asyncio.sleep(0.1)
        n = n - 1

async def f_1(param):
    m = param
    while m >= 0:
        print('[f_1] ' + str(m))
        await asyncio.sleep(0.1)
        m = m - 1

loop = asyncio.get_event_loop()

tasks = [
    f_0(5),
    f_1(3)
]

loop.run_until_complete(asyncio.wait(tasks))
loop.close()

返回

[f_0] 5
[f_1] 3
[f_0] 4
[f_1] 2
[f_0] 3
[f_1] 1
[f_0] 2
[f_1] 0
[f_0] 1
[f_0] 0

另一个例子

import asyncio
import aiohttp

async def request(session, url):
    async with session.get(url) as response:
        return await response.read()

async def fetch(url):
    await asyncio.sleep(1)
    async with aiohttp.ClientSession() as session:
        html = await request(session, url)
        print(html)

url_list = [
    "http://www.qq.com",
    "http://www.jianshu.com",
    "http://www.cnblogs.com"
]

tasks = [fetch(url) for url in url_list]

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

可以看到需要加上 async 表示支持异步调用,并且要用 await 指定被挂起的地方
如果 await 指定的代码无法被挂起的话,是会出错的
并且需要使用特定的异步方法,或是类

相比较而言 gevent 则可以做到对程序透明
一个正常的同步程序,不需要任何修改就可以通过 gevent 实现异步

但 gevent 是借助三方包,asyncio 则是 python 标准库,在语法层面提供支持



最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容

  • 协程 阅读目录 一 引子 二 协程介绍 三 Greenlet模块 四 Gevent模块 引子 之前我们学习了线程、...
    go以恒阅读 705评论 0 1
  • 参考资料 http://www.gevent.org/contents.html https://uwsgi-do...
    JunChow520阅读 16,854评论 0 10
  • 真正有知识的人的成长过程,就像麦穗的成长过程:麦穗空的时候,麦子长得很快,麦穗骄傲地高高昂起,但是,麦穗成熟饱满时...
    IT未来家阅读 1,568评论 2 19
  • 一、总体内容 1.1、协程的介绍 1.2、迭代器以及迭代器的应用 1.3、生成器(生成器与迭代器保存的都是生成数据...
    IIronMan阅读 861评论 0 1
  • 一、python协程概念的引入 之前我们学习了线程、进程的概念,了解了在操作系统中进程是资源分配的最小单位,线程是...
    SlashBoyMr_wang阅读 290评论 0 2