• 重新谈异步编程-协程


    首先来说什么是协程?

    协程又被称之为是微线程,或者说是在一个线程内实现代码块的相互切换执行。

    在《计算机操作系统》中我们学过,一个进程中包含若干个线程,一个线程中可以包含若干个进程。在Python中,一个线程又包含若干个协程。CPU如果在进程和进程之间切换,开销是比较大的,相对来讲,同一进程下的线程切换开销要小很多,因为同一进程下的线程是共享该进程的资源的。同理,同一线程下的协程切换,也要比线程切换开销小。

    协程不像线程一样,需要创建线程实例化对象,协程是通过协程函数+将协程调入事件循环实现的。

    协程函数的定义关键词是asyncio

    例如:

    import asyncio
    async def fun():
        pass

    这就是一个协程函数的定义,asyncio模块是内置的,不用额外下载安装。但是协程函数不能像普通函数一样,“fun()”,可以运行,而是需要将协程函数注册到事件循环当中,并且协程函数中必须有“await+可等待对象(协程对象、Future对象、Task对象)”。

    例如:

    import asyncio
    async def fun():
        print('hello')
        await asyncio.sleep(2)
        print('word')
    loop = asyncio.get_event_loop()
    loop.run_until_complete(fun())

    其中,asyncio.get_event_loop()是创建事件循环对象,loop.run_until_complete(fun())是将协程函数fun()注册到事件循环当中,并运行。结果是没有问题的,输出'hello',等待2s,在输出'word‘。

    当中的asyncio.sleep(2),是模拟IO工作2s,await是将这个操作挂起2s中,或者说这个操作被阻塞2s的时间。这里的asyncio.sleep(2)不能写成time.sleep(2),两个虽然都是等待2s,但是实质是不一样的。

    再py3.7之后,我们可以将

    loop = asyncio.get_event_loop()

    loop.run_until_complete(fun())

    合并为:

    asyncio.run(fun())

    ============================================================================================================================

    现在,我们在创建一个新的协程函数。

    import asyncio
    import time
    async def fun():
        print('hello')
        await asyncio.sleep(2)
        print('word')
    async def fun1():
        print('how are you?')
        await asyncio.sleep(2)
        print('fine')
    start_time = time.time()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(fun())
    loop.run_until_complete(fun1())
    end_time = time.time()
    print('耗时',end_time-start_time)

    输出:

    hello
    word
    how are you?
    fine
    耗时 4.002251625061035

    我们发现,虽然是将两个协程函数分别注册到了时间循环当中,但是并没有像我们想象中一样,交替执行,而是仍然串行执行,消耗4s。我们希望是fun协程先执行print('hello'),遇到了一个2s的IO操作,马上切换到协程fun1的print('how are you?'),又碰到IO操作,继续将IO操作挂起。经过不到2s的时间后协程fun的IO已经结束了,继续执行fun的print('word'),再执行fun1的print('fine')。为什么是这样?我个人猜测和理解,并没有证明,loop.run_until_complete(fun())、loop.run_until_complete(fun1())是先后注册到协程中的,fun()结束之后,才运行fun1()。如果有其他解释欢迎留言。这里我们还要正确理解await后边的IO操作。IO操作是可以和CPU并行进行的,也就是说在同一时刻,既可以CPU计算,又可以IO工作。如果说在IO的时候,CPU在等待,很明显浪费时间,所以我们希望有IO和CPU并行进行,也就是有IO的协程之间并发。

    ============================================================================================================================

    那我们如何将两个协程并发起来呢?依照上边的思想,我们希望将两个协程同时注册到事件循环当中。

    例:

    import asyncio
    import time
    async def fun():
        print('hello')
        await asyncio.sleep(2)
        print('word')
    async def fun1():
        print('how are you?')
        await asyncio.sleep(2)
        print('fine')
    start_time = time.time()
    task1 = asyncio.ensure_future(fun())
    task2 = asyncio.ensure_future(fun1())
    task_list = [task1,task2]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(task_list))
    end_time = time.time()
    print('耗时',end_time-start_time)

    输出:

    hello
    how are you?
    word
    fine
    耗时 2.0022101402282715

    我们发现,这是我们需要的。代码中的asyncio.ensure_future()用来创建task对象,task_list是一个列表,列表里边每个元素是task对象。

    我们打印一下asyncio.wait(task_list),输出:<coroutine object wait at 0x000001E8C9B523C8>,发现这是一个协程对象(注意不是协程函数)。

    这里有同学说了,我们将协程函数转成协程对象不就可以了?print(fun()),发现fun()确实是协程对象,但是会报警告,“Enable tracemalloc to get the object allocation traceback”。

    所以在这里,实际上是将协程对象先转成了task对象,再将task对象组成的列表传入到loop.run_until_complete()里。

    所以到此为止我们知道了,loop.run_until_complete()这个方法的参数必须是一个协程对象。

    我们可以将asyncio.wait(task_list)改为,res = asyncio.gather(*task_list)也可以,wait方法要求传入的是一个列表,gather方法要求传入的参数是一个列表解包后的结果。所以也可以这样写:asyncio.gather(task1,task2)。

    import asyncio
    import time
    async def fun():
        print('hello')
        await asyncio.sleep(2)
        print('word')
    async def fun1():
        print('how are you?')
        await asyncio.sleep(2)
        print('fine')
    
    start_time = time.time()
    task1 = asyncio.ensure_future(fun())
    task2 = asyncio.ensure_future(fun1())
    task_list = [task1,task2]
    loop = asyncio.get_event_loop()
    res = asyncio.gather(task1,task2)
    print(res)
    loop.run_until_complete(res)
    end_time = time.time()
    print('耗时',end_time-start_time)

    输出:

    <_GatheringFuture pending>
    hello
    how are you?
    word
    fine
    耗时 2.0014095306396484

    这里我们发现小细节,asyncio.gather(task1,task2)的返回值并不是前边说的task类对象,而是一个Future类对象,这是因为task是Future的子类,这下就都明白了吧?pending的意思是未开始,打印的时候还没有开始run呢。所以说gather比wait更高一级。

    那么task对象到底有什么用?task对象是把协程对象进行封装,可以追踪 coroutine协程对象的完成状态。也就是说保存了协程运行后的状态,用于未来获取协程的结果。

    到此,我们知道了loop.run_until_complete()的参数可以是Future对象,也可以是协程对象。回想第一个例子,我们把fun()传入到这里不就可以解释了吗?

    ============================================================================================================================

    现在,我们来说说ensure_future和create_task区别。

    两个方法都是来创建task对象,但是如果把上边的ensure_future修改为create_task会报错。为啥?

    注意:如果当前线程中没有正在运行的事件循环,asyncio.create_task将会引发RuntimeError异常。可以理解为asyncio.create_task必须写在一个协程函数中。

    将上面的代码略微修改就可以:

    import asyncio
    import time
    async def fun():
        print('hello')
        await asyncio.sleep(2)
        print('word')
    async def fun1():
        print('how are you?')
        await asyncio.sleep(2)
        print('fine')
    async def main():
        start_time = time.time()
        task1 = asyncio.create_task(fun())
        task2 = asyncio.ensure_future(fun1())
        await asyncio.gather(task1,task2)
        end_time = time.time()
        print('耗时',end_time-start_time)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

    输出:

    hello
    how are you?
    word
    fine
    耗时 2.000105619430542

    在main()协程函数中,await是必须要有的,如果没有await,这就不是一个正确的协程函数,会报错。后边的asyncio.gather(task1,task2)前边介绍过,返回的是一个Future对象,是没问题的。我是从实用角度解释的ensure_future和create_task区别,并不全面,后续碰到实际问题再实际分析。

    ============================================================================================================================

    刚才说的到的asyncio.wait 和 asyncio.gather,有什么区别呢?

    前者的返回值是两个集合结果,第1个集合是已经完成的task,第2个集合是未完成的task。后者直接返回的是多个task的计算结果。

    例:

    import asyncio
    import time
    async def fun():
        print('hello')
        await asyncio.sleep(2)
        print('word')
    async def fun1():
        print('how are you?')
        await asyncio.sleep(2)
        print('fine')
    async def main():
        start_time = time.time()
        task1 = asyncio.create_task(fun())
        task2 = asyncio.ensure_future(fun1())
        res1,res2 = await asyncio.wait([task1,task2])
        print(res1)
        print('======')
        print(res2)
        end_time = time.time()
        print('耗时',end_time-start_time)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

    输出:

    hello
    how are you?
    word
    fine
    {<Task finished coro=<fun() done, defined at C:/Users/王栋轩/PycharmProjects/pythonProject1/main.py:98> result=None>, <Task finished coro=<fun1() done, defined at C:/Users/王栋轩/PycharmProjects/pythonProject1/main.py:102> result=None>}
    ======
    set()
    耗时 2.000847101211548
    
    进程已结束,退出代码0

    很容看到,返回值的第1个是2个协程对象,第2个是空。我们可以使用task.result来获取两个task对象(两个task对象是由前边的两个协程函数封装好的)返回值。

    例:

    import asyncio
    import time
    async def fun():
        print('hello')
        await asyncio.sleep(2)
        print('word')
        return 'fun1'
    async def fun1():
        print('how are you?')
        await asyncio.sleep(2)
        print('fine')
        return 'fun2'
    async def main():
        start_time = time.time()
        task1 = asyncio.create_task(fun())
        task2 = asyncio.ensure_future(fun1())
        res1,res2 = await asyncio.wait([task1,task2])
        for i in res1:
            print(i.result())
        end_time = time.time()
        print('耗时',end_time-start_time)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

    输出:

    hello
    how are you?
    word
    fine
    <class '_asyncio.Task'> fun2
    <class '_asyncio.Task'> fun1
    耗时 2.0008704662323
    
    进程已结束,退出代码0

    例:

    import asyncio
    import time
    async def fun():
        print('hello')
        await asyncio.sleep(2)
        print('word')
        return 'fun1'
    async def fun1():
        print('how are you?')
        await asyncio.sleep(2)
        print('fine')
        return 'fun2'
    async def main():
        start_time = time.time()
        task1 = asyncio.create_task(fun())
        task2 = asyncio.ensure_future(fun1())
        res1,res2 = await asyncio.gather(*[task1,task2])
        print(res1,res2)
        end_time = time.time()
        print('耗时',end_time-start_time)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

    输出:

    hello
    how are you?
    word
    fine
    fun1 fun2
    耗时 2.000060558319092

    可以看到,代码中用res1和res2分别接收task1和task2的返回结果。

    ok。到此,协程的又一步学习告一段落。

    耗时1个 晚上+一个1下午。

    ending……

  • 相关阅读:
    HDU5772 (最小割)
    HDU 4971 (最小割)
    暑期集训个人赛1
    HDU 5644 (费用流)
    HDU5619 (费用流)
    暑假集训热身赛
    构建之法阅读笔记05
    找小水王
    找水王
    Runner站立会议之个人会议(冲刺二)
  • 原文地址:https://www.cnblogs.com/lgwdx/p/15508963.html
Copyright © 2020-2023  润新知