zoukankan      html  css  js  c++  java
  • Python协程(一) 概述

    一、协程介绍

    协程 ,又被称为微线程或者纤程,是一种用户态的轻量级线程,英文名Coroutine,它是实现多任务的一种方式。

    其本质就是一个单线程,协程的作用就是在一个线程中人为控制代码块的执行顺序。

    具体解释如下:
    在一个线程中有很多函数,我们称这些函数为子程序。当一个子程序A在执行过程中可以中断执行,切换到子程序B,执行子程序B。而在适当的时候子程序B还可以切换回子程序A,去接着子程序A之前中断的地方(即回到子程序A切换到子程序B之前的状态)继续往下执行,这个过程,我们可以称之为协程。

    二、Yield生成器的方式实现协程

    在Python中,yield(生成器)可以很容易的实现上述的功能,从一个函数切换到另外一个函数。

    由于比较繁琐,这里不再赘述,可以参考:https://blog.csdn.net/weixin_41599977/article/details/93656042

    三、Greenlet模块

    Greenlet是一个用C实现的协程模块,相比于python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator。

    安装:

    pip3 install greenlet
    使用:
    from greenlet import greenlet
    import time
    
    def task_1():
        while True:
            print("--This is task 1!--")
            g2.switch()  # 切换到g2中运行
            time.sleep(0.5)
    
    def task_2():
        while True:
            print("--This is task 2!--")
            g1.switch()  # 切换到g1中运行
            time.sleep(0.5)
            
    if __name__ == "__main__":
        g1 = greenlet(task_1)  # 定义greenlet对象
        g2 = greenlet(task_2)
        
        g1.switch()  # 切换到g1中运行

    运行输出:

    --This is task 1!--
    --This is task 2!--
    --This is task 1!--
    --This is task 2!--
    --This is task 1!--
    --This is task 2!--
    --This is task 1!--
    --This is task 2!--

    四、Gevent模块

    Greenlet已经实现了协程,但是这个需要人工切换,很麻烦。

    Python中还有一个能够自动切换任务的模块gevent,其原理是当一个greenlet遇到IO操作(比如网络、文件操作等)时,就自动切换到其他的greenlet,等到IO操作完成,在适当的时候切换回来继续执行。

    由于IO操作比较耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程 ,就保证总有greenlet在运行,而不是等待IO。

    安装:

    pip3 install gevent

    用法:

    g1=gevent.spawn(func,1,2,3,x=4,y=5)
    # 创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的,spawn是异步提交任务
    
    g2=gevent.spawn(func2)
    
    g1.join() #等待g1结束
    
    g2.join() #等待g2结束  有人测试的时候会发现,不写第二个join也能执行g2,是的,协程帮你切换执行了,但是你会发现,如果g2里面的任务执行的时间长,但是不写join的话,就不会执行完等到g2剩下的任务了
    
    #或者上述两步合作一步:
    gevent.joinall([g1,g2])
    
    g1.value #拿到func1的返回值

    遇到IO阻塞时会自动切换任务:

    import gevent
    
    def eat(name):
        print('%s eat 1' % name)
        gevent.sleep(2)
        print('%s eat 2' % name)
    
    def play(name):
        print('%s play 1' % name)
        gevent.sleep(1)
        print('%s play 2' % name)
    
    
    g1 = gevent.spawn(eat, 'egon')
    g2 = gevent.spawn(play, name='egon')
    g1.join()
    g2.join()
    # 或者gevent.joinall([g1,g2])
    

    上例gevent.sleep(2)模拟的是gevent可以识别的IO阻塞。

    time.sleep(2)或其他的阻塞,gevent是不能直接识别的。需要用下面一行代码打补丁,就可以识别了:

    from gevent import monkey;
    monkey.patch_all() #必须放到被打补丁者的前面,如time,socket模块之前

    或者我们干脆认为:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头:

    from gevent import monkey
    
    monkey.patch_all()  # 必须写在最上面,这句话后面的所有阻塞全部能够识别了
    
    import gevent  # 直接导入即可
    import time
    
    def eat():
        # print()  
        print('eat food 1')
        time.sleep(2)  # 加上monkey就能够识别到time模块的sleep了
        print('eat food 2')
    
    def play():
        print('play 1')
        time.sleep(1)  # 来回切换,直到一个I/O的时间结束,这里都是我们个gevent做得,不再是控制不了的操作系统了。
        print('play 2')
    
    g1 = gevent.spawn(eat)
    g2 = gevent.spawn(play)
    gevent.joinall([g1, g2])
    print('')

    协程是通过自己的程序(代码)来进行切换的,只有遇到协程模块能够识别的IO操作的时候,程序才会进行任务切换,实现并发效果,如果所有程序都没有IO操作,那么就基本属于串行执行了。

    五、Python3.x协程

    Python3.x系列的协程有很多不同的地方,这里介绍下主要的:

    1、asyncio

    • asyncio是Python3.4引进的标准库,直接内置了对IO的支持,asyncio的操作,需要在coroutine中通过yield from完成。
    import asyncio
    
    
    @asyncio.coroutine
    def get_body(i):
        print(f'start{i}')
        yield from asyncio.sleep(1)
        print(f'end{i}')
    
    
    loop = asyncio.get_event_loop()
    tasks = [get_body(i) for i in range(5)]
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()

    输出结果:

    start4
    start0
    start1
    start3
    start2
    end4
    end0
    end1
    end3
    end2

    它的效果是和Gevent一样的,遇到IO操作的时候,自动切换上下文。

    不同的是,它对tasks的操作:task先把这个5个参数不同的函数全部加载进来,然后执行任务,任务执行是无序的。

    @asyncio.coroutine把一个generator标记为coroutine类型,然后把这个coroutine扔到eventloop中执行

    yield from 语法让我们方便的调用另一个generator。由于asyncio.sleep()也是一个coroutine,线程不会等待,直接中断执行下一个消息循环。当asyncio.sleep()返回时,线程可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。

    2、async/await

    在Python3.5的时候,asyncio添加了两个关键字aysncawait,让coroutine语法更简洁。
    async关键字可以将一个函数修饰为协程对象,await关键字可以将需要耗时的操作挂起,一般多是IO操作。
    它们是针对coroutine的新语法,只需要把@asyncio.coroutine替换为asyncyield from替换为await
    import asyncio
    
    
    async def get_body(i):
        print(f'start{i}')
        await asyncio.sleep(1)
        print(f'end{i}')
    
    
    loop = asyncio.get_event_loop()
    tasks = [get_body(i) for i in range(5)]
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()

    运行结果:

    start3
    start4
    start1
    start0
    start2
    end3
    end4
    end1
    end0
    end2

    Python3.7以后的版本使用asyncio.run即可。此函数总是会创建一个新的事件循环并在结束时关闭之。它应当被用作 asyncio 程序的主入口点,理想情况下应当只被调用一次。

    import asyncio
     
    async def work(x):  # 通过async关键字定义一个协程
        for _ in range(3):
            print('Work {} is running ..'.format(x))
    
    coroutine_1 = work(1)  # 协程是一个对象,不能直接运行
    
    # 方式一:
    loop = asyncio.get_event_loop()  # 创建一个事件循环
    result = loop.run_until_complete(coroutine_1)  # 将协程对象加入到事件循环中,并执行
    print(result)  # 协程对象并没有返回结果,打印None
    # 方式二:
    # asyncio.run(coroutine_1)  #创建一个新的事件循环,并以coroutine_1为程序的主入口,执行完毕后关闭事件循环

    使用asyncio实现的协程的一些特性:

    • 使用async修饰返回的协程对象,不能直接执行,需要添加到事件循环event_loop中执行。
    • 协程主要是用于实现并发操作,其本质在同一时刻,只能执行一个任务,并非多个任务同时展开。
    • 协程中被挂起的操作,一般都是异步操作,否则使用协程没有啥意义,不能提高执行效率。
    • 协程是在一个单线程中实现的,其并发并未涉及到多线程。

    六、为什么要使用协程

    我们广泛使用的Python解释器是CPython,而CPython解释器中存在GIL锁,它的作用就是防止多线程时线程抢占资源,所以在同一时间只允许一个线程在执行,即使在多核CPU情况下也是一样 ,所以CPU的单核和多核对于多线程的运行效率并没有多大帮助,还要在线程之间的不停切换。

    基于以上情况,在一些多线程的场景时,我们就可以使用协程来代替多线程,并且可以做的更灵活。我们下面来看下协程的优势:
    1、线程是系统调度的,协程是程序员人为调度的,更加灵活,简化编程模型
    2、与多线程相比,协程无需上下文切换的开销,避免了无意义的调度,提高了性能
    3、与多线程相比,协程不需要像线程一样,无需原子操作锁定和同步的开销

    所以,在处理一些高并发场景时,有时协程比多线程更加适合,比如做爬虫时。

    参考文章:

    https://www.jianshu.com/p/334388949ac9

    https://blog.csdn.net/weixin_41599977/article/details/93656042

    https://blog.csdn.net/weixin_44251004/article/details/86594117

    https://www.cnblogs.com/cheyunhua/p/11017057.html

    https://www.cnblogs.com/russellyoung/p/python-zhi-xie-cheng.html

    https://www.cnblogs.com/dbf-/p/11143349.html

  • 相关阅读:
    记录下python学习中,容易弄混和实用的知识点
    操作系统简史
    计算机结构
    计算机结构
    电脑简史
    电脑简史
    为什么学Python
    为什么学Python
    树莓派更换更新国内源
    树莓派更换更新国内源
  • 原文地址:https://www.cnblogs.com/mazhiyong/p/13502286.html
Copyright © 2011-2022 走看看