• 异步编程(四)-----协程


    什么是协程?百度上一大堆,随时可以查。我认为协程就是微线程,比线程还要小。为什么要引入协程?我们发现在线程使用中,有一个GIL锁,线程之间访问临界资源是互斥的,这都是不利于提升代码执行效率的。我们知道线程是CPU调度的最小单位,如果我们有一个线程,线程内包含多个协程,协程之间来回切换就设计不到CPU的切换,就会减小很多不必要的开销。协程和线程相比,切换是由代码的关键字完成的,代码自由度要高一些。协程的使用要比线程、进程麻烦一点。协程在处理IO繁忙型数据时,效率很高,一个线程可以并发上完个协程。说白了,在做异步爬虫时,效率很高。下边逐步开始探索。

    目前python实现协程的方式大概有3中:yield关键字、asyncio模块、gevent模块。

    yield这个词在操作系统中出现过,学过操作系统的同学对这个词应该不陌生。下边上代码:

    def func1():
        print('111111')
        yield
        print('222222')
        yield
    if __name__ == '__main__':
        gen1 = func1()
        gen1.__next__()
        gen1.__next__()

    输出:

    111111
    222222

    关键字yield的使用和迭代器就有关系了。在函数定义func1()有关键字yield,所以就不在是一个普通的函数定义。如果仍然用func1()调用执行函数,会发现没有任何输出。print(func1())之后,打印出来的是一个

    <generator object func1 at 0x0000000003967248>

    哦,generator 是生成器!如何执行这个生成器呢,就用.__next__()方法,或者用next(func1())也可以。

    再来一个例子:

    def func1():
        print('111111')
        yield
        print('222222')
        yield
    
    def func2():
        print('333333')
        yield
        print('444444')
        yield
    if __name__ == '__main__':
        gen1 = func1()
        gen2 = func2()
        gen1.__next__()
        gen2.__next__()
        gen1.__next__()
        gen2.__next__()

    输出:

    111111
    333333
    222222
    444444

    好像看起来通过yield关键字,可以实现两个函数之间的来回切换。确实,核心点在于,调用生成器方法.__next__() 会执行到函数yield部分,下次的.__next__() 会继续执行。这是比较简单的yield用法,但是能看出来确实起到协程切换的作用的了。用的不多。

    gevent模块是很古老的python用于实现协程的模块,还在2.x的时代。在3.7之后,就出现asycio模块代替gevent模块了,所以gevent模块在这里就不在讨论。有新的,为什么还用旧的?早晚被淘汰。

    asyncio是python3.7之后自带的,不用额外pip下载,直接在代码中import就可以。

    下边着重说asyncio模块:

    asyncio模块主要基于关键字async、await。

    用async关键词修饰的函数叫做协程函数:

     这是最基本的协程函数定义,调用。

    刚才的代码可以这样写:

    import asyncio
    async def fun():
        print("hello world!")
    
    if __name__ == '__main__':
        reslut = fun()
        loop = asyncio.get_event_loop()
        loop.run_until_complete(reslut)

    asyncio.run(reslut)

    等同于:
    loop = asyncio.get_event_loop()

    loop.run_until_complete(reslut)

    为了实现协程之间来回切换,必须创建一个“圈”,规定圈内的协程可以来回切换,这个“圈”就是asyncio.get_event_loop()创建的事件循环对象。好比是之前说的线程池,只有线程池内部的线程才可以切换。

    loop.run_until_complete(reslut) 是将协程函数注册到事件循环中,也就是添加到线程池中,这样才能被执行。

    再来一个例子,说明下await:

    import asyncio
    async def fun1():
        print('111111')
        await asyncio.sleep(1)
        print('222222')
    
    async def fun2():
        print('333333')
        await asyncio.sleep(1)
        print('444444')
    
    if __name__ == '__main__':
        reslut1 = fun1()
        reslut2 = fun2()
        loop = asyncio.get_event_loop()
        loop.run_until_complete(reslut1)
        loop.run_until_complete(reslut2)

    输出:

    111111
    222222
    333333
    444444

    从输出上看出来,好像没有完成并发操作,仍然是串行,那是因为代码写的是串行的代码。await 后边必须要跟能够等待的对象,asyncio.sleep()就是模拟IO等待的一个函数。如果写time.sleep()会报错。

    await类似于前边的yield,和中断也很像,就是等,停下来执行await后边的函数,这么说和函数调用也差不多。。。

    那问题来了,你不是要实现并发吗,怎么写就可以呢?

    下边要是用task对象。

    import asyncio
    import time
    async def fun1():
        print('111111')
        await asyncio.sleep(1)
        print('222222')
    
    async def fun2():
        print('333333')
        await asyncio.sleep(1)
        print('444444')
    
    async def main():
        reslut1 = fun1()
        reslut2 = fun2()
        task1 = asyncio.create_task(reslut1)
        task2 = asyncio.create_task(reslut2)
        await task1
        await task2
    if __name__ == '__main__':
        start_time = time.time()
        asyncio.run(main())
        end_time = time.time()
        print(end_time-start_time)

    输出:

    111111
    333333
    222222
    444444
    1.0020570755004883

    从输出和时间来看,两个函数fun1和fun2实现了并发执行。

     result1和result2是协程对象,自己可以print一下试试。asyncio.create_task(reslut1)、asyncio.create_task(reslut2) 是创建task对象,同时将任务添加到事件循环,这里其实是做了两件事!!!!所以在代码中并没有看到有关于“将任务添加到事件循环”。

    再继续:

    import asyncio
    import time
    async def fun1():
        print('111111')
        await asyncio.sleep(1)
        print('222222')
        return '1212'
    
    async def fun2():
        print('333333')
        await asyncio.sleep(1)
        print('444444')
        return '3434'
    
    async def main():
        print("main开始")
        reslut1 = fun1()
        reslut2 = fun2()
        task_list = [
            asyncio.create_task(reslut1),
            asyncio.create_task(reslut2)
        ]
        done,pending = await asyncio.wait(task_list)
        for ret in done:
            print('Task ret: ', ret.result())
        print("main结束")
        print(done)
    if __name__ == '__main__':
        start_time = time.time()
        asyncio.run(main())
        end_time = time.time()
        print(end_time-start_time)

    输出:

    main开始
    111111
    333333
    222222
    444444
    Task ret:  1212
    Task ret:  3434
    main结束
    {<Task finished coro=<fun1() done, defined at C:/Users/W/PycharmProjects/test/class/协程.py:22> result='1212'>, <Task finished coro=<fun2() done, defined at C:/Users/W/PycharmProjects/test/class/协程.py:28> result='3434'>}
    1.0020573139190674

    done,pending = await asyncio.wait(task_list) 是关键代码,task_list是一个task对象的列表,asyncio.wait执行事件监听里的task对象。返回的done是一个set集合,集合中包含协程函数的返回值,也就是可以以这样的方式获取返回值,或者利用之前的回调机制,也可以。

     gather也可以实现wait功能,具体方法略有不同,请参考:

    python—异步IO(asyncio)协程
  • 相关阅读:
    NOIP模拟 1
    wlan
    IS-IS IGP
    linux 基础 软件的安装 *****
    第五部分 linux 软件安装RPM SRPM与YUM
    第四部分 linux使用者管理
    添加rpmforge源 centos 7
    x86 保护模式 十 分页管理机制
    X86保护模式 八操作系统类指令
    poj2230 Watchcow【欧拉回路】【输出路径】(遍历所有边的两个方向)
  • 原文地址:https://www.cnblogs.com/lgwdx/p/14304379.html
Copyright © 2020-2023  润新知