博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Python学习笔记 - asyncio
阅读量:5876 次
发布时间:2019-06-19

本文共 8109 字,大约阅读时间需要 27 分钟。

一. 协程

我对协程的理解就是在单线程中执行函数A,可以中断函数A的执行,切换去执行其他函数,在合适的时候(由程序自己控制)在切换回A继续执行,达到类似多线程的效果。

这样做的好处是:

  1. 没有线程的切换,所以不会有线程切换的开销。
  2. 因为只有一个线程,所以修改共享的变量不需要锁
  3. 协程之间的切换由程序控制而不是操作系统控制

二. 协程的效率

  • 计算密集型

    在计算密集型的程序中,协程的效率并不是很高,反而会变低:

    from threading import Threadimport timeimport queuedef work(q):    res = 0    for i in range(1000000):        res += i ** 2    q.put(res)def mutil_thread():    res = 0    threads = []    q = queue.Queue()    for i in range(2):        t = Thread(target=work, args=(q,))        t.start()        threads.append(t)    [thread.join() for thread in threads]    while not q.empty():        res += q.get()    print(res)def coroutine_consumer():    n = 0    while True:        m = yield n        if not isinstance(m, int):            return        n += m ** 2def coroutine_producer(consumer):    consumer.send(None)    for i in range(2):        for i in range(1000000):            res = consumer.send(i)    consumer.close()    print(res)def normal():    res = 0    for i in range(2):        for j in range(1000000):            res += j ** 2    print(res)t1 = time.time()normal()print('单线程:%fs' % (time.time() - t1))t2 = time.time()mutil_thread()print('多线程:%fs' % (time.time() - t2))t3 = time.time()coroutine_producer(coroutine_consumer())print('协程:%fs' % (time.time() - t3))复制代码

    结果:

    可以看到协程是最慢的

  • IO密集型

    import timeimport asyncioimport threadingdef work(t):    print('task %d start' % t)    time.sleep(t)   # 想象这是一个费时的io操作    print('task %d end' % t)def normal():    [work(i) for i in range(1, 3)]def multi_thread():    threads = []    for i in range(1, 3):        t = threading.Thread(target=work, args=(i,))        t.start()        threads.append(t)    [thread.join() for thread in threads]async def work_coroutine(t):    print('task %d start' % t)    await asyncio.sleep(t)  # 想象这是一个费时的io操作    print('task %d end' % t)async def main(loop):    tasks = [loop.create_task(work_coroutine(t)) for t in range(1, 3)]    await asyncio.wait(tasks)t1 = time.time()normal()print('单线程:%fs' % (time.time() - t1))t2 = time.time()loop = asyncio.get_event_loop()loop.run_until_complete(main(loop))loop.close()print('协程:%fs' % (time.time() - t2))t3 = time.time()multi_thread()print('多线程:%fs' % (time.time() - t3))复制代码

    结果:

    协程是和多线程的速度差不多的,但是协程是在一个线程里实现的,没有线程切换的开销,所以线程越多,协程的性能优势就越明显。

三. 关于生成器的一些总结

之前在生成器和迭代器中总结过这方面的一些知识,但是看到协程的使用,好多大神都会用使用了yield的函数作为例子讲解,但是我并不是很理解。所以在这里继续总结一些之前没有总结到的知识点:

  • 生成器的执行顺序

    我一直以为使用了yield的函数(也就是生成器),是执行到了yield返回yield后面的跟着的值,然后继续执行下面的代码,然后第二次执行的时候再从头开始,其实顺序不是这样的,是第一次执行到yield处,然后返回yield后面的跟着的值,函数就暂停了,等下一次再执行的时候就从上一次暂停的yield处继续执行,例子:

    def my_generator(max):n = 0while n <= max:    print('yield前')    yield n    print('yield后')    n += 1g = my_generator(10)print(next(g))print('=' * 10)print(next(g))复制代码

    结果:

    可以看到第一次使用next获取迭代器g的值得时候,是到了yield前然后返回,就停了,第二次则是从上次停止的地方继续执行下面的代码,碰到yield返回,函数暂停。

  • next,send,close,throw

    生成器有几种状态,可以通过getgeneratorstate方法看到:

    GEN_CREATED # 等待执行GEN_RUNNING # 正在执行GEN_SUSPENDED # 暂停(在yield处)GEN_CLOSED # 执行结束复制代码

    当生成器第一次被调用的时候是没有办法拿到yield返回的结果的:

    from inspect import getgeneratorstatedef my_generator(max):    n = 0    while n <= max:        yield n        n += 1g = my_generator(10)print(g)  # 
    print(getgeneratorstate(g)) # GEN_CREATED复制代码

    想要拿到yield返回的结果,就要用的send或者next方法,相当于激活(启动)生成器。例子:

    def my_generator(max):    n = 0    while n < max:        m = yield n        n += mg = my_generator(10)print(next(g))   # 0print(g.send(4)) # 4print(g.send(8)) # 8复制代码

    next和send方法都可以拿到生成器yield返回的结果,它们的不同就是send可以带一个参数,这个参数指的是上一次yield语句的返回值。分析一下上面的例子就明白了:

    # 生成器def my_generator(max):    n = 0    while n < max:        m = yield n        n += m        # 创建一个generator对象,并没有执行yield语句g = my_generator(10)'''激活生成器,并执行它,执行到yield n,返回n,注意m = yield n的执行顺序是从右到左,也就是yield n之后函数就暂停了m这时候并没有被赋值。这时候n = 0'''print(next(g))'''send 方法指定了一个上次 yield n 的返回值为4,这已经是第二次执行生成器了所以从上次停止的地方开始,上次是停在了给m赋值的操作,yield n返回值被指定为4,所以m被赋值为4,继续执行n += m这句代码,这时候n = 4, while语句的条件为True,所以继续循环到了 m = yield n这句代码,同样从右到左的顺序,执行 yield n 返回n的值4'''print(g.send(4))'''以此类推,上次还还是停在了给m赋值的操作,send方法的参数是8,所以上次 yield n 这句代码返回的就是8,把8赋值给m,继续执行,当再一次执行到m = yield n的时候,n就是12了。'''print(g.send(8))复制代码

    如果上面解释的还是不清楚的话,可以这样理解,就是 yield n 这句话会让生成器返回 n的值,然后yield n这句话本身也会返回一个值,send方法的参数就是指定yield n这句话本身的返回值。

    有时候还会看到这样的代码:

    example_generator.send(None)复制代码

    send(None)就好像第一次调用生成器的next(example_generator)一样,都可以激活一个生成器,只不过激活的时候send的参数只能是None。例子:

    def my_generator(max):n = 0while n < max:    m = yield n    n = mg = my_generator(100)# g.send(None) == next(g)print(g.send(None))  # 0print(g.send(4))  # 4print(g.send(8)) # 12复制代码

    close方法就和它的名字一样,关闭一个生成器。当关闭之后再通过next或者send方法就会报StopIteration。例子:

    def my_generator(max):n = 0while n < max:    m = yield n    n = mg = my_generator(100)print(g.send(None))  print(g.send(4))g.close()print(g.send(8))复制代码
    # 结果04Traceback (most recent call last):    File "test.py", line 16, in 
    print(g.send(8))StopIteration复制代码

    throw方法可以结束生成器执行,并抛出指定异常或系统定义异常

    def my_generator(max):try:     n = 0    while n < max:        m = yield n        n = mexcept ValueError:    print('ValueError')g = my_generator(100)print(g.send(None))  # 0print(g.send(4))     # 4g.throw(ValueError)  # ValueError复制代码

四. asyncio

说了那么多,终于到了asyncio,asyncio是Python内置的一个标准库,是Python3.4版本之后引入的,也就是Python内置了对异步IO的支持。

asyncio的使用:

  1. async/await 关键字

    给函数前面加上 async 关键字可以定义一个协程,直接调用这个协程的函数,并不会直接执行这个函数,而是返回一个coroutine对象,而且还会引发一个RuntimeWarning的警告

    import asyncioasync def say_hello():    print('Hello')    await asyncio.sleep(1)复制代码
    # 结果
    test.py:8: RuntimeWarning: coroutine 'say_hello' was never awaited print(say_hello())RuntimeWarning: Enable tracemalloc to get the object allocation traceback复制代码

    await 则是挂起一个耗时的操作,也就是告诉Python这一步是耗时的,主线程不用等待这个操作,可以切换去执行其他协程。

    例子:

    import asyncioasync def say_hello(i):    print('Hello start %d ' % i)    await asyncio.sleep(1)    print('Hello end %d' % i)async def main(loop):    tasks = [loop.create_task(say_hello(i)) for i in range(1, 3)]    await asyncio.wait(tasks)loop = asyncio.get_event_loop()loop.run_until_complete(main(loop))复制代码

    结果:

    Hello start 1Hello start 2Hello end 1Hello end 2复制代码

    使用await,主线程就不会去等待asyncio.sleep(1)这个操作了,它会转去执行其他协程。只有协程, Task 和 Future才可以被await可以成功挂起,比如 await time.sleep(1)就不会被挂起

  2. asyncio.sleep

    asyncio.sleep有点像time.sleep,但是time.sleep会阻塞主线程,这个不会,它会阻塞当前的任务,让Python去执行其他的任务,参数就阻塞的时间。

  3. loop.create_task

    说loop.create_task之前,最好先知道这两个概念:

    Future对象: 包含了异步操作结果的对象。

    Task 对象:继承自Future,它是对Future和协程的进一步封装,用于事件循环。

    我这个是抄别人博客中的解释,

    说实话还是不理解这个两个概念,目前的理解是,协程函数不能直接运行,会报警告(之前的例子中有),想要运行协程函数就得将它包装成Task 对象。包装的方法有两个:

    • asyncio.ensure_future(coro_or_future, *, loop=None)
    • loop.create_task(coroutine)

    官方推荐使用 loop.create_task,ensure_future接受future对象或者协程对象,create_task只接受协程对象。

  4. asyncio.wait

    asyncio.wait是同时运行任务的方法,和它很像的还有一个asyncio.gather。看下它们两的区别:

    import asyncioasync def foo(i):    return i ** iasync def work_1(loop):    tasks = [asyncio.create_task(foo(i)) for i in range(4)]    done, pending = await asyncio.wait(tasks)    print(done, pending)    for task in done:        print(task.result())        loop = asyncio.get_event_loop()loop.run_until_complete(work_1(loop))复制代码
    # 结果:{
    result=1>,
    result=1>,
    result=4>,
    result=27>} set()11427复制代码
    • 第一个参数是包含 coroutines 或 futures 的可迭代对象。
    • 执行是无序的。
    • 返回值是完成的任务和未完成的任务,通过result方法获取任务的结果
    import asyncioasync def foo(i):    return i ** iasync def work_2(loop):    tasks = [asyncio.create_task(foo(i)) for i in range(4)]    result = await asyncio.gather(*tasks)    print(result)loop = asyncio.get_event_loop()loop.run_until_complete(work_2(loop))复制代码
    # 结果:[1, 1, 4, 27]复制代码
    • 第一个参数是任意个 coroutines 或 futures。
    • 执行是有序的
    • 返回值是已完成任务的结果
  5. loop = asyncio.get_event_loop

    得到当前上下文的事件循环。

    这句话一下子搞了两个对我来说很模糊的概念,就是事件循环和上下文:

    • 事件循环:

      在计算系统中,可以产生事件的实体叫做事件源,能处理事件的实体叫做事件处理者。此外,还有一些第三方实体叫做事件循环。它的作用是管理所有的事件,在整个程序运行过程中不断循环执行,追踪事件发生的顺序将它们放到队列中,当主线程空闲的时候,调用相应的事件处理者处理事件。

    • 上下文:

      上下文是一段程序运行所需要的最小数据集合。

    在协程中,把协程函数注册(放入)事件循环中,当事件发生的时候就会调用对应的协程函数。

  6. loop.run_until_complete

    运行直到传入的Future对象完成。run_until_complete方法可以接受Future对象也可以接受协程对象,如果传入的是协程对象,它会帮你转换成Future对象。

  7. loop.close()

    关闭事件循环。

基本上是把例子中用到的都说了,虽然还是有点不清楚,先记下来,下一步就是应用在爬虫上,希望可以在用的时候加深理解。

转载于:https://juejin.im/post/5c4958ad6fb9a04a0a5f899f

你可能感兴趣的文章
007-Shell test 命令,[],[[]]
查看>>
关于Linux系统使用遇到的问题-1:vi 打开只读(readonly)文件如何退出保存?
查看>>
pandas 按照某一列进行排序
查看>>
在WPF中如何使用RelativeSource绑定
查看>>
Map的深浅拷贝的探究
查看>>
XSLT语法 在.net中使用XSLT转换xml文档示例
查看>>
如何将lotus 通讯簿导入到outlook 2003中
查看>>
WinForm 应用程序中开启新的进程及控制
查看>>
前端工程师的职业发展路线在哪?
查看>>
IOS 内存警告 Memory warning level
查看>>
[转]PAC Manager: Ubuntu 上强大的 SSH 帐号管理工具,可取代 SecureCRT_Miracle_百度空间...
查看>>
顺序容器 (2)string类型操作
查看>>
转载:我最近的研究成果(IGeometry.Project and IGeometry.SpatialReference)
查看>>
提示框
查看>>
HDOJ1233 畅通工程之一(最小生成树-Kruscal)
查看>>
14Spring_AOP编程(AspectJ)_环绕通知
查看>>
PHP之打开文件
查看>>
iOS - OC SQLite 数据库存储
查看>>
PHP-mysqllib和mysqlnd
查看>>
Redis常用命令
查看>>