Python 协程

100 阅读4分钟

C10K 瓶颈,也就是同时连接到服务器的客户达到了一万个

事件循环

事件循环启动一个统一的调度器,让调度器来决定一个时刻去运行哪个任务,于是省却了多线程中启动线程、管理线程、同步锁等各种开销

回调地狱(callback hell

这种工具完美地继承了事件循环的优越性,同时还能提供 async / await 语法糖,解决了执行性和可读性共存的难题

Python 2 使用生成器实现协程 --老方法

Python 3.7 提供了新的基于 asyncio 和 async / await 的方法

代码示例1

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

这里的 crawl_page 和 main 都是异步函数, 调用异步函 便可得到一个协程对象(coroutine object)

协程的执行方式

通过 await 调用。

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

await 是同步调用,因此 crawl_page(url) 在当前的调用结束之前,是不会触发下一次调用的。

代码中

await asyncio.sleep(sleep_time) 会在这里休息若干秒

await crawl_page(url) 则会执行 crawl_page() 函数

asyncio.create_task() 来创建任务

asyncio.run

让Python 的协程接口变得非常简单,不用去理会事件循环怎么定义和怎么使用的问题

好的编程规范是,asyncio.run(main()) 作为主程序的入口函数,在程序运行周期内,只调用一次asyncio.run。

代码示例2

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.create_task 来创建任务。任务创建后很快就会被调度执行,代码也不会阻塞在任务这里。

所以,我们要等所有任务都结束才行,用for task in tasks: await task 即可

代码示例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

代码示例4 解密协程运行时

import asyncio

async def worker_1():
    # step4 worker_1开始运行
    print('worker_1 start')
    
    # step5  运行到await asyncio.sleep(1)时 事件调度器将worker_1 任务切出 
    await asyncio.sleep(1)
    
    # step9 1秒后 调调度器将控制权交给worker_1, 任务完成 从事件循环中退出
    print('worker_1 done')
    
    # step9 worker_1 任务完成 从事件循环中退出

async def worker_2():
    # step7 worker_2开始运行
    print('worker_2 start')
    
    # step8 当前任务切出 事件调度器将worker_2 任务切出
    await asyncio.sleep(2)
    
    # step11 2秒后 调度器将控制权交给worker2 任务完成 从事件循环中退出
    print('worker_2 done')

async def main():
    # step2 创建task1 task2 并且进入循环时间等待
    task1 = asyncio.create_task(worker_1())
    task2 = asyncio.create_task(worker_2())
    
    print('before await')
    # step3 await task1执行时 代表用户选择从当前的主任务中切出,事件调度器开始调度 worker_1这个任务
    await task1
    print('awaited worker_1')
    
    # step6 await task2时 事件调度器开始调度 worker_2这个任务
    await task2
    print('awaited worker_2')
    
    ## end 协程任务全部结束 事件循环结束 调度器结束

# step1 程序进入 main() 函数,事件循环开启
%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

step1-8

以上所有事件 step1 -- step8 的运行时间,都应该在 1ms 到 10ms 之间,甚至可能更短,事件调度器从这个时候开始暂停调度

step9

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

step10

await task1 完成,事件调度器将控制器传给主任务,输出 'awaited worker_1',·然后在 await task2 处继续等待;

step11

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

step12

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

代码示例5 - 协程任务限定运行时间

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