python核心技术与实战学习笔记(十二):python协程

12.1 协程的概念

根据维基百科给出的定义,“协程 ,英文Coroutines,是一种比线程更加轻量级的存在,是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂停或开始执行程序”。它是实现并发编程的一种方式。

以往这种方式都是由多线程和锁来实现,但是线程之间的切换需要用到操作系统内核中的TCB(Thread Control Block)模块来改变线程的状态,这一过程需要耗费一定的CPU资源。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
在这里插入图片描述
协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

12.2 协程的代码实现

下面以一个简化的爬虫代码为例,描述如何让一个同步执行的爬虫变为异步执行的爬虫。
同步执行的爬虫:

import time

def crawl_page(url): # 简化爬虫的scrawl_page函数为休眠数秒,休眠时间取决于url最后的那位数,即下面的1,2,3,4
    print('crawling {}'.format(url))
    sleep_time = int(url.split('_')[-1])
    time.sleep(sleep_time)  
    print('OK {}'.format(url))

def main(urls):
    for url in urls:
        crawl_page(url)

%time main(['url_1', 'url_2', 'url_3', 'url_4'])

########## 输出 ##########

crawling url_1
OK url_1
crawling url_2
OK url_2
crawling url_3
OK url_3
crawling url_4
OK url_4
Wall time: 10 s

同步执行的爬虫依次爬取了4个页面,加起来一共用了10s,效率低下,这种爬取操作完全可以并发化:

import asyncio

async def crawl_page(url):
    print('crawling {}'.format(url))
    sleep_time = int(url.split('_')[-1])
    await asyncio.sleep(sleep_time)
    print('OK {}'.format(url))

async def main(urls):
    tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
    for task in tasks:
        await task

%time asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))

########## 输出 ##########

crawling url_1
crawling url_2
crawling url_3
crawling url_4
OK url_1
OK url_2
OK url_3
OK url_4
Wall time: 3.99 s

可见使用协程写异步程序非常简单,只需改变几处写法就行。

  • import asyncio,这个库包含了大部分我们实现协程所需要的魔法工具
  • async声明异步函数,于是,这里的crawl_page和main都变成了异步函数,而调用异步函数,我们便可以得到一个协程对象(coroutine object)。
  • 协程的执行由多种方法,上面代码中主要用到了await、asyncio.create_task()和asyncio.run()三种。
  1. await:执行效果和python正常执行是一样的,也即程序会堵塞在这里,进入被调用的协程函数,执行完返回再继续,而这也是await的字面意思。代码中await asyncio.sleep(sleep_time)会在这里休息若干秒,await crawl_page(url)则会执行crawl_page()函数
  2. asyncio.create_task():有了协程对象后,便可用来创建任务。任务创建后很快就会被调度执行,这样就使得代码不会堵塞在任务这里。
  3. asyncio.run():触发运行,一个非常好的编程规范是,asyncio.run(main())作为主程序的入口函数,在程序运行周期内,只会调用一次asyncio.run

对于执行task,有两种方法:
一个是如上面代码所示的:

async def main(urls):
    tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
    for task in tasks:
        await task

另一个就是使用asyncio.gather()函数:

async def main(urls):
    tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
    await asyncio.gather(*tasks) 

要注意的是,*tasks表示解包列表,将列表变成函数的参数;与之对应的是,**dict将字典变为函数的参数

12.3 解析协程如何运行

import asyncio

async def worker_1():
    print('worker_1 start')
    await asyncio.sleep(1)
    print('worker_1 done')

async def worker_2():
    print('worker_2 start')
    await asyncio.sleep(2)
    print('worker_2 done')

async def main():
    print('before await')
    await worker_1()
    print('awaited worker_1')
    await worker_2()
    print('awaited worker_2')

%time asyncio.run(main())

########## 输出 ##########

before await
worker_1 start
worker_1 done
awaited worker_1
worker_2 start
worker_2 done
awaited worker_2
Wall time: 3 s

以上代码没有创建task,即用异步接口写同步代码,故执行时间为两个时间的总和。

import asyncio

async def worker_1():
    print('worker_1 start')
    await asyncio.sleep(1)
    print('worker_1 done')

async def worker_2():
    print('worker_2 start')
    await asyncio.sleep(2)
    print('worker_2 done')

async def main():
    task1 = asyncio.create_task(worker_1())
    task2 = asyncio.create_task(worker_2())
    print('before await')
    await task1
    print('awaited worker_1')
    await task2
    print('awaited worker_2')

%time asyncio.run(main())

########## 输出 ##########

before await
worker_1 start
worker_2 start     # 注意这里与第一个代码不同,此处实现了并发执行
worker_1 done
awaited worker_1
worker_2 done
awaited worker_2
Wall time: 2.01 s

对于第二个代码,以下是详细执行步骤:
在这里插入图片描述

取消超时协程任务和处理出错的协程任务

如以下代码所示,work_1正常运行,work_2运行中出现错误,work_3执行时间过长被取消,这些信息最终会全部体现在最终的返回结果res中

import asyncio

async def worker_1():
    await asyncio.sleep(1)
    return 1

async def worker_2():
    await asyncio.sleep(2)
    return 2 / 0

async def worker_3():
    await asyncio.sleep(3)
    return 3

async def main():
    task_1 = asyncio.create_task(worker_1())
    task_2 = asyncio.create_task(worker_2())
    task_3 = asyncio.create_task(worker_3())

    await asyncio.sleep(2)
    task_3.cancel()

    res = await asyncio.gather(task_1, task_2, task_3, return_exceptions=True)
    print(res)

%time asyncio.run(main())

########## 输出 ##########

[1, ZeroDivisionError('division by zero'), CancelledError()]
Wall time: 2 s

对于以下代码:

res = await asyncio.gather(task_1, task_2, task_3, return_exceptions=True)

其中的参数return_exception=True,若不设置这个参数,错误就会完整地throw到我们这个执行层,从而需要用try except来捕捉,这样会使得其他还没被执行的任务会被全部取消掉。为了避免这个局面,将参数return_exception设置为True即可

用协程实现经典的生产消费者模型:

import asyncio
import random

async def consumer(queue, id):
    while True:
        val = await queue.get()
        print('{} get a val: {}'.format(id, val))
        await asyncio.sleep(1)

async def producer(queue, id):
    for i in range(5):
        val = random.randint(1, 10)
        await queue.put(val)
        print('{} put a val: {}'.format(id, val))
        await asyncio.sleep(1)

async def main():
    queue = asyncio.Queue()

    consumer_1 = asyncio.create_task(consumer(queue, 'consumer_1'))
    consumer_2 = asyncio.create_task(consumer(queue, 'consumer_2'))

    producer_1 = asyncio.create_task(producer(queue, 'producer_1'))
    producer_2 = asyncio.create_task(producer(queue, 'producer_2'))

    await asyncio.sleep(10)
    consumer_1.cancel()
    consumer_2.cancel()
    
    await asyncio.gather(consumer_1, consumer_2, producer_1, producer_2, return_exceptions=True)

%time asyncio.run(main())

########## 输出 ##########

producer_1 put a val: 5
producer_2 put a val: 3
consumer_1 get a val: 5
consumer_2 get a val: 3
producer_1 put a val: 1
producer_2 put a val: 3
consumer_2 get a val: 1
consumer_1 get a val: 3
producer_1 put a val: 6
producer_2 put a val: 10
consumer_1 get a val: 6
consumer_2 get a val: 10
producer_1 put a val: 4
producer_2 put a val: 5
consumer_2 get a val: 4
consumer_1 get a val: 5
producer_1 put a val: 2
producer_2 put a val: 8
consumer_1 get a val: 2
consumer_2 get a val: 8
Wall time: 10 s

12.4 总结

  1. 协程与线程的区别,主要在于两点,一是协程为单线程;二是协程由用户决定,在那些地方交出控制权,切换到下一个任务
  2. 协程的写法更加简洁清晰,把async / await语法和create_task结合来用,对于中小级别的并发需求已经毫无压力
  3. 写协程程序时,脑海里要有清晰的事件循环概念,知道程序在什么时候需要暂停、等待I/O,什么时候要一并执行到底

协程不一定总是最优选择,多线程也有其有点,应该要懂得在什么时候用什么模型能达到工程上的最优。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值