协程和异步I/O

协程的概念

协程(coroutine)通常又称之为微线程或纤程,它是相互协作的一组子程序(函数)。所谓相互协作指的是在执行函数A时,可以随时中断去执行函数B,然后又中断继续执行函数A。注意,这一过程并不是函数调用(因为没有调用语句),整个过程看似像多线程,然而协程只有一个线程执行。协程通过yield关键字和 send()操作来转移执行权,协程之间不是调用者与被调用者的关系。

协程的优势在于以下两点:

1.执行效率极高,因为子程序(函数)切换不是线程切换,由程序自身
控制,没有切换线程的开销。
2.不需要多线程的锁机制,因为只有一个线程,也不存在竞争资源的问
题,当然也就不需要对资源加锁保护,因此执行效率高很多。

说明:协程适合处理的是I/O密集型任务,处理CPU密集型任务并不是它的长处,如果要提升CPU的利用率可以考虑“多进程+协程”的模式。

历史回顾:

1.Python 2.2:第一次提出了生成器(最初称之为迭代器)的概念
(PEP 255)。
2.Python 2.5:引入了将对象发送回暂停了的生成器,这一特性即生
3.Python 3.3:添加了yield from特性,允许从迭代器中返回任何
值(注意生成器本身也是迭代器),这样我们就可以串联生成器并且重
构出更好的生成器。
4.Python 3.4:引入asyncio.coroutine装饰器用来标记作为协程
的函数,协程函数和asyncio及其事件循环一起使用,来实现异步I/O
操作。
5.Python 3.5:引入了async和await,可以使用async def来定义
一个协程函数,这个函数中不能包含任何形式的yield语句,但是可以
使用return或await从协程中返回值。

协程运作的流程:

函数内部的中断和函数之间的更替执行,是靠send  和 yield 实现
的 send发送消息给yield,并且中端程序的执行, yield不仅可以接
收send发送的消息还可以中断程序,并且返回给send消息 ,"告诉该
你执行了!",send接收到消息后接着执行中断的程序,到结束进行下次
循环时循环到 send 下面流程就是重复的了

实例示范

  1. 生成器 - 数据的生产者

    from time import sleep  
    
    # 倒计数生成器
    
    def countdown(n):
        while n > 0:
            yield n
            n -= 1
    
    
    def main():
        for num in countdown(5):
            print(f'Countdown: {num}')
            sleep(1)
        print('Countdown Over!')
    
    
    if __name__ == '__main__':
        main()
    生成器还可以叠加来组成生成器管道,代码如下所示。
    
    
    # Fibonacci数生成器
    
    def fib():
        a, b = 0, 1
        while True:
            a, b = b, a + b
            yield a     
    
    # 偶数生成器
    
    def even(gen):
        for val in gen:
            if val % 2 == 0:
                yield val
    
    
    def main():
        gen = even(fib())
        for _ in range(10):
            print(next(gen))
    
    if __name__ == '__main__':
        main()
  2. 协程 - 数据的消费者 
    示例01:

    
    from time import sleep
    
    
    # 生成器 - 数据生产者
    
    def countdown_gen(n, consumer):
        consumer.send(None)
        while n > 0:
            consumer.send(n)
            n -= 1
        consumer.send(None)
    
    
    # 协程 - 数据消费者
    
    def countdown_con():
        while True:
            n = yield
            if n:
                print(f'Countdown {n}')
                sleep(1)
            else:
                print('Countdown Over!')
    
    
    def main():
        countdown_gen(5, countdown_con())
    
    if __name__ == '__main__':
        main()

    示例02:

    
    # 消费者 快递员
    
    from random import randint
    from time import sleep
    from myutils import coroutine
    
    
    
    # 激活消费者的装饰器 自己自动激活, 这样就不用在生成器中每次都激活了
    
    @coroutine
    def create_delivery_man(name, capacity=1):
        buffer = []
        # 不知道要消费多少东西
        while True:
            # 接收派送给快递员的东西
            size = 0
            while size < capacity:
                pkg_name = yield
                if pkg_name:
                    size += 1
                    buffer.append(pkg_name)
                    print('%s正在接收%s' % (name, pkg_name))
                else:
                    break
            print('======%s正在派送%d件包裹' % (name, len(buffer)))
            sleep(3)
            buffer.clear()
    
    
    def create_package_center(consumer, max_packages):
        # 为了让消费者函数的代码停留在 n=yield 这一句,等着接收数据
        # consumer.send(None)
        num = 0
        while num <= max_packages:
            print('快递中心准备派送%d号包裹' % num)
            consumer.send('包裹-%d' % num)
            num += 1
            if num % 10 == 0:
                sleep(5)
        # consumer.send(None)
    
    
    def main():
        dm = create_delivery_man('杨二狗', 7)
        create_package_center(dm, 25)
    
    
    if __name__ == '__main__':
        main()
    

    说明:上面代码中countdown_gen函数中的第1行consumer.send(None)是为了激活生成器,通俗的说就是让生成器执行到有yield关键字的地方挂起,当然也可以通过next(consumer)来达到同样的效果。如果不愿意每次都用这样的代码来“预激”生成器,可以写一个包装器来完成该操作,代码如下所示。

    from functools import wraps
    
    def coroutine(fn):
    
        @wraps(fn)
        def wrapper(*args, **kwargs):
            gen = fn(*args, **kwargs)
            next(gen)
            return gen
    
        return wrapper

    这样就可以使用@coroutine装饰器对协程进行预激操作,不需要再写重复代码来激活协程

  3. 异步I/O - 非阻塞式I/O操作

    import asyncio
    
    
    @asyncio.coroutine
    def countdown(name, n):
        while n > 0:
            print(f'Countdown[{name}]: {n}')
            yield from asyncio.sleep(1)
            n -= 1
    
    
    def main():
        loop = asyncio.get_event_loop()
        tasks = [
            countdown("A", 10), countdown("B", 5),
        ]
        loop.run_until_complete(asyncio.wait(tasks))
        loop.close()
    
    
    if __name__ == '__main__':
        main()
  4. async和await

    import asyncio
    import aiohttp
    
    async def download(url):
        print('Fetch:', url)
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as resp:
                print(url, '--->', resp.status)
                print(url, '--->', resp.cookies)
                print('\n\n', await resp.text())
    
    
    def main():
        loop = asyncio.get_event_loop()
        urls = [
            'https://www.baidu.com',
            'http://www.sohu.com/',
            'http://www.sina.com.cn/',
            'https://www.taobao.com/',
            'https://www.jd.com/'
        ]
        tasks = [download(url) for url in urls]
        loop.run_until_complete(asyncio.wait(tasks))
        loop.close()
    
    
    if __name__ == '__main__':
        main()

    上面的代码使用了AIOHTTP这个非常著名的第三方库,它实现了HTTP客户端和HTTP服务器的功能,对异步操作提供了非常好的支持,有兴趣可以阅读它的官方文档

猜你喜欢

转载自blog.csdn.net/weixin_37657720/article/details/81187820