python协程 生成器

2020-08-07T20:35:41
关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9

协程,又称微线程,纤程。英文名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

 

扫一扫关注公众号添加购物返利助手,领红包
当前页面是本站的「Baidu MIP」版。发表评论请点击:完整版 »
因本文不是用Markdown格式的编辑器书写的,转换的页面可能不符合MIP标准。