美文网首页
Python 协程:yield,greenlet,gevent,

Python 协程:yield,greenlet,gevent,

作者: moon_light_ | 来源:发表于2020-04-19 04:09 被阅读0次
    进程

    进程是资源分配的最小单位,拥有独立的内存空间,有寄存器信息、堆、栈、数据段、代码段、虚拟内存、文件句柄、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 标准库,在语法层面提供支持


    相关文章

      网友评论

          本文标题:Python 协程:yield,greenlet,gevent,

          本文链接:https://www.haomeiwen.com/subject/ohjzvhtx.html