Qingquan-Li / blog

My Blog
https://Qingquan-Li.github.io/blog/
132 stars 16 forks source link

Python 协程 #169

Open Qingquan-Li opened 3 years ago

Qingquan-Li commented 3 years ago

参考:


一、协程简介

Coroutine 协程子例程的更一般形式。 子例程可以在某一点进入并在另一点退出。 协程则可以在许多不同的点上进入、退出和恢复。

协程,又称微线程,纤程。一句话说明什么是线程:协程是一种用户态的轻量级线程。

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。

协程使用发展史:

随着互联网的快速发展,你逐渐遇到了 C10K 瓶颈,也就是同时连接到服务器的客户达到了一万个。于是很多代码跑崩了,进程上下文切换占用了大量的资源,线程也顶不住如此巨大的压力,这时, NGINX 带着事件循环出来拯救世界了(在高并发下能保持低资源低消耗高性能): 事件循环启动一个统一的调度器,让调度器来决定一个时刻去运行哪个任务,于是省却了多线程中启动线程、管理线程、同步锁等各种开销。

再到后来,出现了一个很有名的名词,叫做回调地狱 callback hell(JavaScript),这种工具完美地继承了事件循环的优越性,同时还能提供 async / await 语法糖,解决了执行性和可读性共存的难题。于是,协程逐渐被更多人发现并看好,也有越来越多的人尝试用 Node.js (JavaScript 是一门单线程的语言,无法多线程实现并发,但可以使用协程实现并发)做起了后端开发。

使用生成器,是 Python 2 开头的时代实现协程的老方法了,Python 3.7 提供了新的基于 asyncio 和 async / await 的方法


进程、线程、协程 的上下文切换比较

进程process 线程thread 协程coroutine
切换者 操作系统(进程是资源分配的最小单位) 操作系统(线程是CPU调度的最小单位) 用户(编程者/应用程序)
切换时机 根据操作系统自己的切换策略,用户不感知 根据操作系统自己的切换策略,用户不感知 用户自己(的程序)决定
切换内容 页面全局目录
内核栈
硬件上下文
内核栈
硬件上下文
硬件上下文
切换内容的保存位置 内核栈 内核栈 用户自己的变量(用户栈或堆)
切换过程 用户态-内核态-用户态 用户态-内核态-用户态 用户态(没有陷入内核态)
切换效率


协程总结:


进程、线程、协程应用场景




二、协程示例

示例1:同步(不使用协程)爬取4个URL

import time
import timeit

def crawl_page(url):
    print('crawling {}'.format(url))
    # 休眠时间取决于 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)

if __name__ == "__main__":
    start = timeit.default_timer()
    main(['url_1', 'url_2', 'url_3', 'url_4'])
    stop = timeit.default_timer()
    print('Time:', stop - start)
$ python sync_crawl
# 输出 crawling url_num 后,等待 num 秒后输出对应的 OK url_num
crawling url_1
OK url_1
crawling url_2
OK url_2
crawling url_3
OK url_3
crawling url_4
OK url_4
Time: 10.012153034

示例2:异步(使用协程)爬取4个URL

import timeit
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)
    # await 是同步调用,因此,crawl_page(url) 在当前的调用结束之前,
    # 是不会触发下一次调用的。这里相当于用异步接口写了个同步代码。
    # docs.python.org/zh-cn/3.8/tutorial/datastructures.html#list-comprehensions
    tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
    for task in tasks:
        await task
    # 以上for循环也可写作如下。其中,*tasks 解包列表,将列表变成了函数的参数;
    # 与之对应的是, ** dict 将字典变成了函数的参数。
    # await asyncio.gather(*tasks)

start = timeit.default_timer()
# asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))
asyncio.run(main(['url_4', 'url_3', 'url_2', 'url_1']))
stop = timeit.default_timer()
print('Time:', stop - start)
$ python async_crawl
# 立刻输出4个 crawling url_num 后,第1秒后输出 OK url_1,第2秒后输出 OK url_2 ... 第4秒后输出 OK url_4
crawling url_4
crawling url_3
crawling url_2
crawling url_1
OK url_1
OK url_2
OK url_3
OK url_4
Time: 4.00415018