Python核心丨协程

Python协程


基础

协程是实现并发编程的一种方式

实例

  • 简单的爬虫
import time

def crawl_page(url):
    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

这是一个简单的爬虫,main()函数执行时,调取crawl_page()函数进行网络通信,经过若干秒等待后收到结果,然后执行下一个。

  • 使用协程的写法
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):
    for url in urls:
        await crawl_page(url)

%time asyncio.run(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

首先import asyncio,这个库包含了大部分实现协程所需的魔法工具。

async修饰词声明异步函数,这里的crawl_page和main都变成了异步函数,而调用异步函数,便可以得到一个协程对象。

例,如果print(crawl_page(’’)),便会输出<coroutine object crawl_page at xxxxxxx>,提示你这是一个Python的协程对象,而并不是会真正执行这个函数。

协程的执行

首先,可以通过await来调用。await执行的效果,和Python正常执行是一样的,也就是说程序会阻塞在这里,进入被调用的协程函数,执行完毕返回后再继续,而这也是await的字面意思。

代码中await asyncio.sllep(sleep_tiem)会在这里休息若干秒,await crawl_page(url)则会执行crawl_page()函数。

其次,可以通过asyncio.create_task()来创建任务。

最后,需要asyncio.run来触发运行。asyncio.run这个函数是Python3.7之后才有的特性。

运行结果还是10秒,是因为await是同步调用,因此,crawl_page(url)再当前的调用结束之前,是不会触发下一次调用。

协程的重要概念:任务(Task)

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

有了协程对象后,便可以通过asyncio.creat_task来创建任务。

任务创建后很快就会被调度执行,这样,代码也不会阻塞再任务这里。

执行task的另一种做法

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]
    await asyncio.gather(*tasks)

%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: 4.01 s

*task解包列表,将列表变成了函数的参数;与之对的是,**dict将字典变成了函数的参数。

注:asyncio.create_task,asyncio.run都是Python3.7以上的版本才提供的。

解密协程运行时
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
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

过程分析

1.asyncio.run(main()),程序进入main()函数,事件循环开启;

2.task1和task2任务被创建,并进入时间循环等待运行;运行到print,输出’before await’;

3.await task1执行,用户选择从当前的主任务切出,时间调度器开始调度worker_1;

4.worker_1开始运行,运行print输出’worker_1 start‘,然后运行到await asyncio.sleep(1),从当前任务切出,时间调度器开始调度worker_2;

5.worker_2开始运行,运行print输出’worker_2 start’,然后运行await asyncio.sleep(2)从当前任务切出;

6.以上所有时间的运行时间,都应该再1ms到10ms之间,甚至可能更短,时间调度器从这个时候开始暂停调度;

7.一秒钟后,worker_1的sleep完成,时间调度器将控制权重新传给task_1,输出’worker_1 done’,task_1完成任务,从事件循环中退出;

8.await task1完成,时间调度器将控制器传给主任务,输出’awaiter worker_1’,然后在await task2处继续等待;

9.两秒中后,worker_2的sleep完成,事件调度器将控制权重新传给task_2,输出’worker_2 done’,task_2完成任务,从事件循环中退出;

10,主任务输出’awaited worker_2’,协程全任务结束,事件循环结束。

示例

  • 想给某些协程任务限定运行事件,一旦超时就取消。
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

worker_1正常运行 ,worker_2运行中出现错误,worker_3执行时间过长被cancel掉,这些信息会全部体现在最终的返回结果res中。

需要注意的是,return_exceptions=True这行代码。如果不设置这个参数,错误就会完整地throw到执行层,从而需要try except来捕捉,这也意味着其他还没有被执行的任务全部取消掉。

为了避免这个情况,将return_exceptions设置为True即可。

小结
  • 协程和多线程的区别,主要在于两点,一是协程为单线程;二是协程由用户决定,在哪些地方交出控制权,切换到下一个任务。

  • 协程的写法更加简洁清晰

  • 写协程程序的时候,要有清晰的事件循环概念,知道程序在什么时候需要暂停、等待I/O,什么时候需要一并执行到底。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值