在 Python 中支持多种异步方式,比如:协程,多线程和多进程,除此之外还有一些比较传统的方法和第三方的异步库。本文主要是介绍协程,捎带会介绍一下多线程和多进程。
async/await
在 Python 中使用 async
声明的函数就是一个异步函数,这个异步函数也尝尝被称之为协程。如下:
import asyncio
async def hello():
await asyncio.sleep(1)
print("hello")
调用方式
异步函数的调用和普通函数的调用有一点不一样,比如普通函数的调用如下:
def hello():
print("hello")
hello()
而异步函数的调用如下:
import asyncio
async def hello():
await asyncio.sleep(1)
print("hello")
h = hello()
asyncio.run(h)
在调用异步函数时,首先使用 h=hello()
,返回的是一个协程对象,而函数中的代码是不会执行的。之后再使用 asycnio.run(h)
函数或者 await h
的代码才会执行,如下所示:
import asyncio
async def async_function():
print("This is inside the async function")
await asyncio.sleep(1)
return "Async function result"
# 正确的使用方式
async def correct_usage():
print("Correct usage:")
result = await async_function()
print(f"Result: {result}")
# 不使用 await 的调用
def incorrect_usage():
print("\nIncorrect usage:")
coroutine = async_function()
print(f"Returned object: {coroutine}")
# 注意:这里不会打印 "This is inside the async function"
# 处理未等待的协程
async def handle_unawaited_coroutine():
print("\nHandling unawaited coroutine:")
coroutine = async_function()
try:
# 使用 asyncio.run() 来运行协程
result = await coroutine
print(f"Result after handling: {result}")
except RuntimeWarning as e:
print(f"Caught warning: {e}")
async def main():
await correct_usage()
incorrect_usage()
await handle_unawaited_coroutine()
asyncio.run(main())
下面介绍几种常见的调用异步函数的方法。
asyncio.gather()
使用 gather
就是同时启动多个任务,并且并发执行,执行完成返回结果之后,后面的代码才会继续执行。如下:
import asyncio
async def num01():
await asyncio.sleep(1)
return 1
async def num02():
await asyncio.sleep(1)
return 2
async def combine():
results = await asyncio.gather(num01(), num02())
print(results)
asyncio.run(combine())
## output:[1,2]
上面有两个异步函数,现在使用 asyncio.gather
同时并发执行这两个函数,然后使用 await
等待结果返回,返回结果就放在了 results
当中;
直接使用 await
上面的 gather
是收集多个异步函数,同时并发执行。除了这种方法以外,也可以直接使用 await
关键字,具体如下:
import asyncio
async def hello():
await asyncio.sleep(1)
return "hello"
async def exmaple():
result = await hello()
print(result)
asyncio.run(exmaple())
# output: hello
在上面的代码 exmaple
中,使用 await
等待异步函数返回结果,返回之后再输出到控制台。这种方式其实就是顺序执行,因为代码执行到 await
时就会等待结果,等返回结果之后才会继续向下执行。
如果此处不等会怎么样呢?如果此处使用 result = hello()
,那么 hello()
中的代码不会被执行,返回的 result
是一个协程对象
asyncio.create_task()
除了上面的方法之外,还有一种更加灵活的方式,就是使用 asyncio.create_task()
。这种方式会创建任务,并且立即在后台执行,此时主函数可以做一些其他的操作。如果需要获取异步任务的结果时,再使用 await
获得得到,具体如下:
import asyncio
async def number():
await asyncio.sleep(1)
return 1
async def float():
await asyncio.sleep(1)
return 1.0
async def exmaple():
n = asyncio.create_task(number())
f = asyncio.create_task(float())
print("do something...")
print(await n)
print(await f)
asyncio.run(exmaple())
# output:
do something...
1
1.0
从上面的输出可以看出, create_task
会先去创建任务并开始执行,此时主函数并不会被阻塞,而是会继续执行下面的代码。等到需要使用异步函数的结果的时候,调用 await n
就可以获取到结果。这样就可以将一些耗时任务先放到异步代码中执行,等到需要这些异步函数的结果的使用,再去获取
注:上面的使用 create_task
调用异步函数和直接像普通函数的方式调用异步函数不一样,使用普通函数的调用方式 number()
,此时函数不会执行。而使用 create_task
调用异步函数,该函数会立即执行,即便不使用 await
获取结果,函数也会在主函数没有退出的情况下执行完成Semaphore
Semaphore
asyncio.Semaphore
是 Python 的 asyncio
库中的一个同步原语,控制对于共享资源的访问。在异步编程中非常有用,可以限制同时访问某个资源的协程数量。如下代码所示:
import asyncio
import aiohttp
async def fetch(url, session, semaphore):
async with semaphore:
print(f"Fetching {url}")
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
"http://example.com",
"http://example.org",
"http://example.net",
"http://example.edu",
"http://example.io",
]
semaphore = asyncio.Semaphore(2) # 限制并发请求数为2
async with aiohttp.ClientSession() as session:
tasks = [fetch(url, session, semaphore) for url in urls]
responses = await asyncio.gather(*tasks)
for url, response in zip(urls, responses):
print(f"URL: {url}, Response length: {len(response)}")
asyncio.run(main())
上面的代码中创建一个 asyncio.Semaphore(2)
,限制同时并发数为2个。在异步函数 fetch
中,使用 async with semaphore
获取和释放信号量。在进入之前会自动调用 acquire()
方法获取,在退出 with
方法时,会调用 release()
方法释放信号量。使用 Semaphore
能够控制并发数,防止对服务器造成压力。在处理有限的资源,如:数据库连接时非常有用。同时能够优化系统性能,找到并发的平衡点。
Semaphore 原理
它在内部维护一个计数器,当计数器大于零时允许访问,当等于零时,禁止访问。获取和释放计数器的方法是分别调用 acquire()
和 release()
。在初始化的时候要指定一个初始的计数器数量。之后在代码中通过控制计数器的数量控制并发数。
多线程
多线程是一种传统的任务并发方式,适用于 I/O
绑定的任务,如下面的这个例子:
import threading
import time
def worker(name):
print(f"Worker {name} starting")
time.sleep(2) # 模拟耗时操作
print(f"Worker {name} finished")
def main():
threads = []
for i in range(3):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
print("All workers finished")
if __name__ == "__main__":
main()
多线程的 t.join()
是等待三个线程的完成。当在一个线程或进程对象上调用 join()
方法时,调用线程(通常是主线程)将被阻塞,直到被调用 join()
的线程或进程执行完毕。
多进程
多进程适用于 CPU 密集的任务,可以充分的理由多核处理器,如下:
import multiprocessing
import time
def worker(name):
print(f"Worker {name} starting")
time.sleep(2) # 模拟耗时操作
print(f"Worker {name} finished")
if __name__ == "__main__":
processes = []
for i in range(3):
p = multiprocessing.Process(target=worker, args=(i,))
processes.append(p)
p.start()
for p in processes:
p.join()
print("All workers finished")
结语
除了上面的异步方式,Python 中还有一些其他的异步方法,比如使用回调函数,或者是第三方的库 Gevent
等。每种方法都有自己的优势和局限,比如线程适合 I/O 绑定的任务,但有 GIL(全局解释器锁)的限制;多进程适合 CPU 密集型任务,但有更大的内存开销;第三方库提供专门的功能和优化,但可能增加项目的复杂性。相比之下,async
/await
语法提供了一种更现代、更易读的异步编程方式,是目前 Python 中处理异步操作的推荐方法。