文档章节

python协程 生成器

沙门行道
 沙门行道
发布于 2019/12/15 18:24
字数 2166
阅读 19
收藏 0

协程,又称微线程,纤程。英文名Coroutine。

线程是系统级别的它们由操作系统调度,而协程则是程序级别的由程序根据需要自己调度。在一个线程中会有很多函数,我们把这些函数称为子程序,在子程序执行过程中可以中断去执行别的子程序,而别的子程序也可以中断回来继续执行之前的子程序,这个过程就称为协程。也就是说在同一线程内一段代码在执行过程中会中断然后跳转执行别的代码,接着在之前中断的地方继续开始执行,类似与yield操作。

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

协程的优点:

  1. 无需线程上下文切换的开销,协程避免了无意义的调度,由此可以提高性能(但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力)
  2. 无需原子操作锁定及同步的开销
  3. 方便切换控制流,简化编程模型
  4. 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

协程的缺点:

  1. 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  2. 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

生成器

要理解协程,首先需要知道生成器是什么。生成器其实就是不断产出值的函数,只不过在函数中需要使用yield这一个关键词将值产出。一般的函数在执行完毕之后会返回一个值然后退出,但是生成器函数会自动挂起,然后重新拾起急需执行,他会利用yield关键字关起函数,给调用者返回一个值,同时保留了当前的足够多的状态,可以使函数继续执行,生成器和迭代协议是密切相关的,迭代器都有一个__next__()__成员方法,这个方法要么返回迭代的下一项,要买引起异常结束迭代。

# 函数有了yield之后,函数名+()就变成了生成器
# return在生成器中代表生成器的中止,直接报错
# next的作用是唤醒并继续执行
# send的作用是唤醒并继续执行,发送一个信息到生成器内部

def create_counter(n):
    print("create_counter")
    while True:
        yield n
        print("increment n")
        n += 1

if __name__ == '__main__':
    gen = create_counter(2)
    print(next(gen))
    for item in gen:
        print('--->:{}'.format(item))
        if item > 5:
            break

print:
create_counter
2
increment n
--->:3
increment n
--->:4
increment n
--->:5
increment n
--->:6

 

python2 协程

Python对协程的支持是通过generator实现的。在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。

传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

 

协程实现的生产者消费者模型

def consumer():
    r = ''
    while True:
        n = yield r #n 为 send 发送来的参数
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        r = '200 OK'

def produce(c):
    c.send(None) #传送参数,并q
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()

if __name__ == '__main__':
    c = consumer()
    produce(c)

运行结果:
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

注意到consumer函数是一个generator,把一个consumer传入produce后:

  1.  首先调用c.send(None)启动生成器;
  2.  然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
  3. consumer通过yield拿到消息,处理,又通过yield把结果传回;
  4. produce拿到consumer处理的结果,继续生产下一条消息;
  5. produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

整个流程无锁,由一个线程执行,produceconsumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

Python 3.5   asyncio

asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。用asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法asyncawait,可以让coroutine的代码更简洁易读。

同步执行的协程

import asyncio
import time
# 使用 main 函数的await 发起两个携程,此时代码仍然是同步的,
# 当第一个await 完成之后 才会启动第二个await 这是他们的运行就和函数是一致的
async def say_after(delay, what):
    await asyncio.sleep(delay)
    print('--->:{}'.format(what))
async def main():
    print(f"started at {time.strftime('%X')}")
    await say_after(1, 'hello')
    await say_after(2, 'world')
    print(f"finished at {time.strftime('%X')}")
if __name__ == '__main__':
    asyncio.run(main())

执行结果:
2019-12-15 18:03:52,719 - asyncio - DEBUG - Using proactor: IocpProactor
started at 18:03:52
hello
world
finished at 18:03:55

并发执行的协程

import asyncio
import time

# 与上一个例子不同的是:这里启动协程 是通过启动 task 任务的方式,这个任务被认为是可等待的对象,因此它们可以并发的运行,本例将比上例节省一秒钟
async def say_after(delay, what):
    # 为什么要使用这种方式来模拟等待?因为 time.sleep(delay) 不被asyncio认为是可等待对象,所以当替换为 time.sleep() 将不会出现预期的
    await asyncio.sleep(delay)
    print('--->:{}'.format(what))

async def main():
    print(f"started at {time.strftime('%X')}")
    # 用于创建协程任务
    task1 = asyncio.create_task(say_after(1,'hello'))
    task2 = asyncio.create_task(say_after(2,'world'))
    # 并发启动任务 虽然并发的执行了,但在Python中 程序会等待最耗时的协程运行完毕后退出,所以这里耗时2秒
    await task1
    await task2
    print(f"finished at {time.strftime('%X')}")
if __name__ == '__main__':
    asyncio.run(main())

执行结果:
2019-12-15 18:18:50,374 - asyncio - DEBUG - Using proactor: IocpProactor
started at 18:18:50
--->:hello
--->:world
finished at 18:18:52

常用的用法

单核上的协程

tasks = [asyncio.create_task(test(1)) for proxy in range(10000)] 创建了任务
[await t for t in tasks] 丢到执行队列里面去

这里共一万个任务,耗时1.2640011310577393秒 

import asyncio
import time

async def test(time):
    await asyncio.sleep(time)

async def main():
    start_time = time.time()
    tasks = [asyncio.create_task(test(1)) for proxy in range(10000)]
    [await t for t in tasks]
    print(time.time() - start_time)

if __name__ == "__main__":
    asyncio.run(main())

执行结果:
2019-12-15 18:15:17,668 - asyncio - DEBUG - Using proactor: IocpProactor
1.1492626667022705

多核上的协程

多核上要用 当然 要用到 《多进程 + 协程》

from multiprocessing import Pool
import asyncio
import time

async def test(time):
    await asyncio.sleep(time)

async def main(num):
    start_time = time.time()
    tasks = [asyncio.create_task(test(1)) for proxy in range(num)]
    [await t for t in tasks]
    print(time.time() - start_time)

def run(num):
    asyncio.run(main(num))

if __name__ == "__main__":
    p = Pool()
    for i in range(4):
        p.apply_async(run, args=(2500,))
    p.close()
    p.join()

执行结果:
1.0610532760620117
1.065042495727539
1.0760507583618164
1.077049970626831

 

© 著作权归作者所有

沙门行道
粉丝 0
博文 28
码字总数 56229
作品 0
南京
私信 提问
Python 3.5 协程究竟是个啥

原文链接 : How the heck does async/await work in Python 3.5? 原文作者 : Brett Cannon 译文出自 : 掘金翻译计划 译者 : @Yushneng 校对者: @L9m,@iThreeKing 作者是 Python 语言的核心开...

好铁
2017/10/23
134
0
《Python分布式计算》第2章 异步编程 (Distributed Computing with Python)

序言 第1章 并行和分布式计算介绍 第2章 异步编程 第3章 Python的并行计算 第4章 Celery分布式应用 第5章 云平台部署Python 第6章 超级计算机群使用Python 第7章 测试和调试分布式应用 第8章...

seancheney
2017/10/11
0
0
玩转 Python 3.5 的 await/async

最近通过的PEP-0492为 Python 3.5 在处理协程时增加了一些特殊的语法。新功能中很大一部分在3.5 之前的版本就已经有了,不过之前的语法并不算最好的,因为生成器和协程的概念本身就有点容易混...

mallon
2015/11/04
1.5W
19
Python函数式编程指南(四):生成器

生成器是迭代器,同时也并不仅仅是迭代器,不过迭代器之外的用途实在是不多,所以我们可以大声地说:生成器提供了非常方便的自定义迭代器的途径。 这是函数式编程指南的最后一篇,似乎拖了一...

fjie
2013/10/23
0
0
从python协程理解tornado异步

博客原文地址:http://www.v2steve.com/2015/05/31/python/pytornadoasync/ 刚接触tornado时候最疑惑的问题就是tornado.gen.coroutine是怎么实现的。如何在代码中用同步格式实现异步效果。看...

__Steve__
2015/05/31
1.3K
0

没有更多内容

加载失败,请刷新页面

加载更多

每天AC系列(六):有效的括号

1 题目 LeetCode第20题,这题比较简单,匹配括号. 2 栈 这是栈的典型应用,括号匹配,当然不需要直接使用栈,使用一个StringBuilder即可: if(s.isEmpty()) return true;char a = s.charAt(0);...

Blueeeeeee
今天
27
0
Spring AOP-06-切入点类型

切入点是匹配连接点的拦截规则。之前使用的是注解@Pointcut,该注解是AspectJ中的。除了这个注解之外,Spring也提供了其他一些切入点类型: • 静态方法切入点StaticMethodMatcherPointcut •...

moon888
昨天
90
0
Class Loaders in Java

1. Introduction to Class Loaders Class loaders are responsible for loading Java classes during runtime dynamically to the JVM (Java Virtual Machine). Also, they are part of the ......

Ciet
昨天
96
0
以Lazada为例,看电商系统架构演进

什么是Lazada? Lazada 2012年成立于新加坡,是东南亚第一电商,2016年阿里投资10亿美金,2017年完成对lazada的收购。 业务模式上Lazada更偏重自营,类似于亚马逊,自建仓储和为商家提供服务...

春哥大魔王的博客
昨天
62
0
【自用】 Flutter Timer 简单用法

dart: void _startTime() async { _timer = Timer(Duration(seconds: sec), () { fun(xxx,yyy,zzz); }); } @override void dispose() { _timer.cancel()......

Tensor丨思悟
昨天
65
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部