大多数Python开发人员可能只接触过同步代码。但是,如果你是一名数据科学家,你可能使用multiprocessing库来并行运行一些计算。如果你是一名Web开发人员,你可能接触过线程中的并发。multiprocessing和threading都是Python中的高级概念,各自有特定的应用领域。
Asyncio 是 Python 库,用于编写使用 async 和 await 语法的并发代码。它主要用于 I/O 密集型任务,例如网页开发或从 API 中获取数据。
除了多进程和多线程之外,Python的并发性家族中还有另一个新成员:asyncio。Asyncio是一个用于使用async/await语法编写并发代码的库。与线程类似,asyncio适用于在实践中非常常见的I/ o密集型任务。我将介绍asyncio的基础知识,并演示如何使用这个新库编写异步代码。
cpu密集型任务和I/ o密集型任务有什么区别?
在开始使用asyncio库之前,了解cpu密集型和I/ o密集型任务是很重要的,因为它们决定了应该使用哪个库来解决您的特定问题。
cpu密集型任务的大部分时间都用cpu进行繁重的计算。如果你是一名数据科学家,需要处理大量数据来训练机器学习模型,那么这是一项cpu密集型任务。在这种情况下,您应该使用多进程来并行运行作业,并充分利用cpu。
I/O密集型任务将大部分时间用于等待I/O响应,这些响应可以是来自网页、数据库或磁盘的响应。如果你正在开发一个网页,其中一个请求需要从api或数据库中获取数据,这是一个I/ o约束任务。对于I/O密集型任务,可以使用线程或asyncio实现并发,以最大限度地减少来自外部资源的等待时间
Asyncio与线程
现在您知道了线程和异步都适用于I/ o密集型任务,那么它们的区别是什么呢?
首先,线程使用多个线程,而asyncio只使用一个。线程更容易理解,因为线程轮流运行代码,从而实现并发性。但是如何用单个线程实现并发呢?
线程通过抢占式多任务实现并发,这意味着我们无法确定何时在哪个线程中运行哪个代码。是操作系统决定哪些代码应该在哪个线程中运行。操作系统可以在线程之间的任何点切换控制权。这就是为什么我们经常看到线程的随机结果。
另一方面,asyncio通过协作多任务实现并发。我们决定可以等待代码的哪一部分,然后切换控制以运行代码的其他部分。这些都是通过await命令在一个线程中完成的。稍后看到代码时,这一点会更清楚。
Asyncio中的协程?
这是asyncio中一个奇特的名字。要解释它是什么并不容易。许多教程根本不解释这个概念,只是用一些代码向您展示它是什么。然而,让我们试着去理解它是什么。我们将从回顾Python的定义开始。
“协程是一种更通用的子程序形式。子程序在一个点进入,在另一个点退出。协程可以在许多不同的地方进入、退出和恢复。”Glossary — Python 3.10.15 documentation
虽然这看起来仍然相当令人困惑,但一旦您对asyncio有了更多的经验,它就会变得更有意义。
在这个定义中,我们可以将子程序理解为函数,尽管两者之间存在差异。通常,函数只在调用时进入和退出一次。不过,Python中有一个特殊的函数generator,可以多次进入和退出。
协程的行为类似于生成器。在较旧版本的Python中,协程是由生成器定义的。这些协程被称为基于生成器的协程。然而,协程现在已经成为Python的原生功能,并且可以使用新的async def语法来定义。尽管基于生成器的协程现在已被弃用,但它们的历史和存在可以帮助我们理解什么是协程,以及如何在代码的不同部分之间切换或生成控制。
即使你不能马上理解所有的概念,也没关系。随着时间的推移,当你使用asyncio库编写和阅读越来越多的异步代码时,它们将变得更加清晰。
如何在Asyncio中定义协程函数
现在已经介绍了基本概念,我们可以编写我们的第一个协程函数:
async def coro_func():
print("hello")
coro_obj = coro_func()
print(type(coro_obj))
coro_func()是一个协程函数,当它被调用时,它将返回一个协程对象,并输出 “coroutine”
你可能已经注意到了,当协程函数被调用时,print函数并没有被调用。如果你使用过生成器,你不会感到惊讶,因为它的行为与生成器函数类似:
def gen_func():
yield "hello,generator"
generator = gen_func()
print(type(generator))
print(next(generator))
为了在生成器中运行代码,你需要迭代它。你可以使用next函数来迭代它
类似地,要运行在协程函数中定义的代码,你需要等待它。然而,你不能像迭代生成器那样等待它。一个协程只能在另一个由async def语法定义的协程中等待:
import asyncio
async def coro_func():
print('hello world')
async def main():
print('In the entrypoint coroutine')
await coro_func()
a = main()
print(type(a))
asyncio.run(main())
在底层,它是由事件循环处理的。然而,使用现代Python,你不需要担心这些细节。
如何在Asyncio的协程函数中返回值
import asyncio
async def coro_func():
print("Hello, asyncio!")
return "123"
async def main():
print("In the entrypoint coroutine.")
s = await coro_func()
print(s)
asyncio.run(main())
如何在Asyncio中并发运行多个协程
import asyncio
from datetime import datetime
async def async_sleep(num):
print(f"Sleeping {num} seconds.")
await asyncio.sleep(num)
async def main():
start = datetime.now()
coro_objs = []
for i in range(1, 4):
coro_objs.append(async_sleep(i))
await asyncio.gather(*coro_objs)
duration = datetime.now() - start
print(f"Took {duration.total_seconds():.2f} seconds.")
asyncio.run(main())
Asyncio中的Async和Aiohttp示例 python3.10
import asyncio
import aiohttp
async def scrape_page(session, url):
print(f"Scraping {url}")
async with session.get(url) as resp:
return len(await resp.text())
async def main():
urls = [
"http://www.baidu.com",
"http://www.baidu.com",
"http://www.baidu.com"
]
tasks = []
async with aiohttp.ClientSession() as session:
for url in urls:
task = asyncio.create_task(scrape_page(session, url))
tasks.append(task)
results = await asyncio.gather(*tasks)
for url, length in zip(urls, results):
print(f"{url} -> {length}")
asyncio.run(main())