Python协程

网友投稿 240 2022-11-22

Python协程

文章​​Python多线程与多进程​​​中介绍了并行,并发,多线程和多进程的概念。多线程 / 多进程是解决并发问题的模型之一,本文要介绍的协程也是实现并发编程的一种方式。 协程使用的异步IO (asynchronous IO)不是多线程或者多进程的,它是一种单线程、单进程的设计。也就是说,协程可以实现并发调度,但它本身并不是并发的(单线程下的”并发“)。相比多线程和多进程,协程没有进程上下文切换导致的资源占用,运行效率更高。

最开始服务器并发使用的是多线程 / 多进程的方式,随着互联网的快速发展,网络用户数大量增长,遇到了C10K 瓶颈,也就是同时连接到服务器的客户端数量超过 10000 个,导致服务器无法提供正常服务,解决这个问题的其中一个方案就是异步编程。NGINX 提出了事件循环,通过启动一个统一的调度器,让调度器来决定一个时刻去运行哪个任务,于是省去了多线程中启动线程、管理线程、同步锁等各种开销。Node.js中使用 async / await 解决回调地狱(callback hell)问题。 Python 2 使用生成器实现协程,Python2.5 中,使用yield 关键字使生成器有了记忆功能,Python 3.7 提供了新的基于 asyncio 和 async / await 的方法。除了Python,协程也在其它语言中得到实现,比如 golang 的 goroutine,luajit 的 coroutine,scala 的 actor 等,本文主要介绍Python中协程的使用方法。 协程(Coroutine)允许执行被挂起与被恢复,在执行任务(task)A时可以随时中断去执行任务B,通过调度器来进行任务自由切换,这一整个过程中只有一个线程在执行。协程是协作式多任务的的轻量级线程,协程之间的切换不需要涉及任何系统调用或任何阻塞调用。 在IO密集型的多线程实现中,如果I/O 操作非常频繁,多线程会进行频繁的线程切换,并且线程数不能无限增加,所以使用协程非常好的方法。python 协程可以使用asyncio 模块实现,下面先来介绍asyncio。

Asyncio

先来区分一下 Sync(同步)和 Async(异步)的概念。

同步指操作一个接一个地执行,下一个操作必须等上一个操作完成后才能执行。异步指不同操作间可以相互交替执行,如果其中的某个操作被 block 了,程序并不会等待,而是会找出可执行的操作继续执行。

Asyncio 是单线程的,它只有一个主线程,但是可以进行多个不同的任务(task),这里的任务,就是特殊的 future 对象。这些任务被一个叫做 event loop 的对象所控制,event loop 对象控制任务的交替执行,直到所有任务完成,可以把这里的任务类比成多线程里的多个线程。 在Python 3.7 以上版本中,可以使用asyncio库来实现协程,可参考官方文档:​​asyncioimport timeasync def worker_1(): print('worker_1 start') await asyncio.sleep(2) print('worker_1 done')async def worker_2(): print('worker_2 start') await asyncio.sleep(1) print('worker_2 done')async def main(): task1 = asyncio.create_task(worker_1()) task2 = asyncio.create_task(worker_2()) tasks = [task1,task2] print('before await') await asyncio.gather(*tasks) # for task in tasks: # await task # print(task._state) start = time.time()asyncio.run(main())end = time.time()print('Running time: %s Seconds'%(end-start)

先来介绍一下代码中使用到的魔法工具:

```python ```

await:同步调用,阻塞程序,执行对应的协程函数。await asyncio.sleep(5)表示程序暂停等待5s,await worker_1() 则会执行 worker_1() 函数,当前的调用执行结束后才触发下一次调用。async 和 await 关键字一般组合使用,如果任务执行的过程需要等待,则将其放入等待状态的列表中,然后继续执行预备状态列表里的任务。asyncio.create_task():创建任务,任务创建后就会被调度执行,进入事件循环等待运行。使用这种方式创建任务后,就不会出现阻塞。await asyncio.gather(*tasks, return_exception=False):运行tasks序列的所有任务,等待所有任务都结束才结束主程序,单星号​​*​​解包任务列表,也可以这样写:

```python for task in tasks: await task ```

asyncio.run:运行,运行时拿到 event loop对象,运行完成后关闭,这是Python3.7+引入的方法。以前的版本可以使用如下方式:

```python loop = asyncio.get_event_loop() try: loop.run_until_complete(asyncio.wait(tasks)) finally: loop.close() ``` 运行一下代码,执行结果:

before awaitworker_1 startworker_2 startworker_2 doneworker_1 doneRunning time: 2.0120482444763184 Second

执行流程如下:

asyncio.run(main()),事件循环开启asyncio.create_task()创建任务task1 和 task2 ,进入事件循环等待运行,打印“before await”。await task1 执行,事件调度器开始调度 worker_1。worker_1 开始运行,运行到 await asyncio.sleep(2), 从当前任务切出,事件调度器开始调度 worker_2。worker_2 开始运行,运行到 await asyncio.sleep(1) ,从当前任务切出。1s后,worker_2 的 sleep 完成,事件调度器将控制权重新传给 task_2,输出 'worker_2 done',task_2 完成任务,从事件循环中退出。事件调度器在 await task1 处继续等待2s后,worker_1 的 sleep 完成,事件调度器将控制权重新传给 task_1,task_1 完成任务,从事件循环中退出;协程所有任务结束,事件循环结束。

到这里,想必你已经知道协程的概念和asyncio的使用方法了,下面来实现一个使用协程爬虫的程序。

协程爬虫

爬虫是一个比较典型的I/O密集型任务,除了使用多线程实现外,也可以用协程来实现。实际上线程能实现的,协程也都能做到。 下面使用协程来实现抓取博客​​ 单线程版本:

import timeimport requestsfrom bs4 import BeautifulSoupdef main(): baseurl = " header = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36' } # init_page = requests.get(url).content init_page = requests.get(url=baseurl, headers=header).content init_soup = BeautifulSoup(init_page, 'lxml') # 获取文章页数 nav_tag = init_soup.find('nav', class_="pagination") page_number_tag = nav_tag.find_all('a', class_="page-number") page_number = int(page_number_tag[1].text) article_num = 0 for num in range(page_number): if num >=1: url = baseurl + f'/page/{num+1}/' else: url = baseurl init_page = requests.get(url=url, headers=header).content init_soup = BeautifulSoup(init_page, 'lxml') all_articles = init_soup.find('div', class_="content index posts-expand") for each_article in all_articles.find_all('header', class_="post-header"): all_a_tag = each_article.find_all('a') article_name = all_a_tag[0].text article_url = all_a_tag[0].attrs['href'] response_item = requests.get(url=baseurl+article_url, headers=header).content soup_item = BeautifulSoup(response_item, 'lxml') time_tag = soup_item.find('time') publish_time = time_tag.text word_tag = each_article.find_all(title="本文字数") word_count = word_tag[0].text word_count = word_count.strip().split('\n')[1] article_num = article_num + 1 print(f'{article_name} {baseurl+article_url} {publish_time} {word_count}') print(f'一共有{article_num}篇博客文章')start = time.time()main()end = time.time()print('Running time: %s Seconds'%(end-start)

执行结果(部分):

markdown基本语法介绍 2021-06-12 6.8kPython中的闭包 2021-06-10 2.4k算法笔记:位运算 2021-06-08 2.8k常见搜索算法(二):二分查找 2021-06-03 1.1k.............一共有124篇博客文章Running time: 107.27503871917725 Second

使用协程(由于requests 库不兼容 asyncio, 下面使用aio库进行接口请求):

import timeimport requestsfrom bs4 import BeautifulSoupimport asyncioimport aiodef fetch_content(url, header): async with aio headers=header, connector=aio ) as session: async with session.get(url) as response: return await response.text()async def main(): baseurl = " header = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36' } article_names, article_urls,publishs_time,words_count = [], [], [], [] init_page = requests.get(url=baseurl, headers=header).content init_soup = BeautifulSoup(init_page, 'lxml') # 获取文章页数 nav_tag = init_soup.find('nav', class_="pagination") page_number_tag = nav_tag.find_all('a', class_="page-number") page_number = int(page_number_tag[1].text) for num in range(page_number): if num >= 1: url = baseurl + f'/page/{num+1}/' else: url = baseurl # article_names, article_urls, publishs_time, words_count = [], [], [], [] init_page = requests.get(url=url, headers=header).content init_soup = BeautifulSoup(init_page, 'lxml') all_articles = init_soup.find('div', class_="content index posts-expand") for each_article in all_articles.find_all('header', class_="post-header"): all_a_tag = each_article.find_all('a') article_name = all_a_tag[0].text article_url = all_a_tag[0].attrs['href'] article_names.append(article_name) article_urls.append(baseurl+article_url) tasks = [fetch_content(url, header) for url in article_urls] article_num = len(article_urls) pages = await asyncio.gather(*tasks) for article_name, article_url, page in zip(article_names, article_urls, pages): soup_item = BeautifulSoup(page, 'lxml') time_tag = soup_item.find('time') publish_time = time_tag.text word_tag = soup_item.find_all(title="本文字数") word_count = word_tag[0].text word_count = word_count.strip().split('\n')[1] print('{} {} {} {}'.format(article_name, article_url,publish_time,word_count)) print(f'一共有{article_num}篇博客文章')start=time.time()asyncio.run(main())end=time.time()print('Running time: %s Seconds'%(end-start)

执行结果(部分):

一共有124篇博客文章Running time: 14.071799755096436 Second

可以看到速度提升了很多。

多线程、多进程和协程如何选择

在​​Python多线程与多进程​​中介绍了多线程和多进程,它们都有各自的应用场景,在实际应用中,如何选择呢?

I/O 密集型任务,并且 I/O 操作很慢,需要很多任务协同实现,使用协程。I/O 密集型任务,但是 I/O 操作很快,只需要有限数量的任务/线程,使用多线程就可以,当然也可以使用协程。CPU 密集型任务,使用多进程。

总结

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:如何在maven打包时,保持方法形参不变,以及保留javadoc注释
下一篇:Java 函数编程详细介绍
相关文章

 发表评论

暂时没有评论,来抢沙发吧~