asyncio 并发编程
asyncio
是 Python 的一个库,用于编写单线程并发代码使用 async
/await
语法。asyncio
提供了一种使用事件循环来编写并发代码的方式,这种方式非常适合于 I/O 密集型任务和协调多个异步操作的场景。
基本概念
- 协程 (Coroutine): 使用
async def
定义的函数。协程函数调用不会立即执行,而是返回一个协程对象,该对象需要被运行在事件循环中。 - 事件循环 (Event Loop):
asyncio
的核心,用于管理和分发事件和任务。事件循环负责执行协程,以及处理异步 I/O, 延时调用等任务。 - 任务 (Task): 用于在事件循环中调度协程执行的未来式对象。通过
asyncio.create_task()
创建任务。
开始使用 asyncio
要使用 asyncio,首先需要导入库,然后定义一些协程,最后在事件循环中运行这些协程。
import asyncio
async def main():
print('Hello')
await asyncio.sleep(1)
print('World')
# Python 3.7+
asyncio.run(main())
并发运行任务
如果你有多个协程需要并发运行,可以使用 asyncio.gather()
来同时调度它们。
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
task1 = asyncio.create_task(say_after(1, 'hello'))
task2 = asyncio.create_task(say_after(2, 'world'))
print('started at', time.strftime('%X'))
# 等待两个任务完成
await task1
await task2
print('finished at', time.strftime('%X'))
asyncio.run(main())
处理异步 I/O
asyncio
提供了对异步 I/O 操作的支持,比如读写文件、网络请求等。这些操作通常通过 await
来挂起当前协程,直到相应的操作完成,从而不会阻塞整个程序的执行。
async def fetch_data():
reader, writer = await asyncio.open_connection('python.org', 80)
writer.write(b'GET / HTTP/1.0\r\n\r\n')
await writer.drain()
data = await reader.read(100)
print(data.decode())
writer.close()
await writer.wait_closed()
asyncio.run(fetch_data())
错误处理
在 asyncio
程序中处理错误通常使用标准的异常处理方法,即 try
/except
块。
async def fetch_data():
try:
reader, writer = await asyncio.open_connection('python.org', 80)
...
except Exception as e:
print(f'An error occurred: {e}')
finally:
writer.close()
await writer.wait_closed()
asyncio.run(fetch_data())
总结
asyncio
是 Python 中处理并发编程的强大工具,特别适合处理 I/O 密集型和高级别并发应用程序。通过使用 async
/await
语法,asyncio
使得异步代码易于编写和理解。
asyncio 事件循环
asyncio
的事件循环是该库的核心组件之一,负责管理和执行异步任务。事件循环可以看作是一个无限循环,它接收并分发事件或任务,并保持程序运行直到所有任务完成或者被取消。事件循环的主要职责包括执行异步协程任务、处理网络IO操作、运行定时任务,以及在必要时调用相应的回调函数。
创建和运行事件循环
在 asyncio
中,通常有几种方式来创建和运行事件循环。
使用 asyncio.run()
从 Python 3.7 开始,asyncio.run()
是运行异步程序的推荐方式。它创建一个新的事件循环,并在循环中运行传入的协程,直到协程完成。完成后,它会关闭事件循环。
import asyncio
async def main():
print('Hello')
await asyncio.sleep(1)
print('World')
asyncio.run(main())
手动管理事件循环
在某些情况下,你可能需要更细粒度地控制事件循环的创建和关闭。这可以通过直接使用事件循环的 API 来实现。
import asyncio
async def main():
print('Hello')
await asyncio.sleep(1)
print('World')
# 获取当前平台的事件循环策略中的事件循环
loop = asyncio.get_event_loop()
try:
# 运行直到协程完成
loop.run_until_complete(main())
finally:
# 关闭事件循环
loop.close()
事件循环的方法
事件循环提供了多种方法来管理异步任务和其他事件:
run_until_complete(future)
: 运行传入的协程直到完成。create_task(coro)
: 将协程封装为一个任务并调度其执行。run_forever()
: 运行事件循环直到stop()
被调用。stop()
: 停止事件循环。close()
: 关闭事件循环。事件循环使用完毕后应当被关闭。
事件循环中的异步IO和其他操作
asyncio
的事件循环支持异步执行多种操作,如网络请求、文件IO等。这些操作通常通过特定的 asyncio
函数实现,例如:
asyncio.sleep()
: 异步版本的 sleep。asyncio.open_connection()
: 用于打开网络连接。asyncio.start_server()
: 启动一个异步服务器。
总结
asyncio
的事件循环是异步编程的核心,它允许代码以非阻塞的方式执行多个操作。通过合理使用事件循环和相关的 asyncio
API,可以构建出高效的异步应用程序。
task取消和子协程调用原理
在 asyncio
中,任务(Task)是对协程的一种封装,使其可以被调度和管理。任务的取消以及子协程的调用是 asyncio
异步编程中的两个重要概念。
任务取消
在 asyncio
中,可以取消一个正在执行的任务。当一个任务被取消时,事件循环会向任务发送一个 CancelledError
异常。如果任务当前正在等待另一个异步操作(如 await asyncio.sleep(1)
),则该操作会被中断,并抛出 CancelledError
异常。
示例代码:
import asyncio
async def cancel_me():
print('cancel_me(): before sleep')
try:
# 这里等待一段时间
await asyncio.sleep(10)
except asyncio.CancelledError:
print('cancel_me(): cancel sleep')
raise
finally:
print('cancel_me(): after sleep')
async def main():
# 创建任务
task = asyncio.create_task(cancel_me())
# 等待一秒后取消任务
await asyncio.sleep(1)
task.cancel()
try:
await task
except asyncio.CancelledError:
print('main(): cancel_me is cancelled now')
asyncio.run(main())
子协程调用原理
在 asyncio
中,一个协程可以调用另一个协程,这通常通过 await
表达式实现。当一个协程通过 await
调用另一个协程时,调用者会被挂起,直到被调用的协程完成执行。这种机制允许实现复杂的异步逻辑,同时保持代码的清晰和简洁。
示例代码:
import asyncio
async def nested():
print("Nested coroutine")
return 42
async def main():
# 直接调用另一个协程
result = await nested()
print(f"Result of nested coroutine: {result}")
asyncio.run(main())
在这个例子中,main
协程调用了 nested
协程,并等待其完成。await
表达式使得 main
协程在 nested
协程完成前暂停执行。
总结
任务的取消和子协程的调用是 asyncio
异步编程中的两个基本操作。任务取消允许你优雅地中断可能长时间运行的异步操作,而子协程的调用则是构建复杂异步应用程序的基石。通过这些机制,asyncio
提供了强大的工具来处理并发和异步编程的复杂性。
call_soon、call_at、call_later、call_soon_threadsafe
在 asyncio
中,事件循环提供了几种方法来安排回调的执行。这些方法允许你在特定时间或尽快执行某个函数,这对于整合非异步代码到异步应用中或者处理定时事件非常有用。
call_soon
call_soon
方法用于尽快调度一个回调函数的执行。它不是立即执行回调,而是将回调放入事件循环的队列中,等待下一次事件循环迭代时执行。
import asyncio
def callback():
print('Callback called!')
loop = asyncio.get_event_loop()
loop.call_soon(callback)
loop.run_until_complete(asyncio.sleep(1))
loop.close()
call_later
call_later
方法用于在指定的时间后调度一个回调函数执行。它接受一个时间延迟(以秒为单位)和要调用的回调函数。
import asyncio
def callback():
print('Callback called!')
loop = asyncio.get_event_loop()
# 在1秒后调用callback
loop.call_later(1, callback)
loop.run_until_complete(asyncio.sleep(2))
loop.close()
call_at
call_at
方法用于在指定的绝对时间点调度一个回调。它接受一个特定的时间戳(相对于事件循环的时间方法,如 loop.time()
)和一个回调函数。
import asyncio
def callback():
print('Callback called!')
loop = asyncio.get_event_loop()
# 当前时间加2秒
when = loop.time() + 2
loop.call_at(when, callback)
loop.run_until_complete(asyncio.sleep(3))
loop.close()
call_soon_threadsafe
call_soon_threadsafe
是线程安全版本的 call_soon
。它用于从非异步代码或不同线程安排回调在异步事件循环中执行。这在你需要从另一个线程与异步代码交互时非常有用。
import asyncio
import threading
def callback():
print('Callback called from', threading.current_thread())
def start_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()
loop = asyncio.new_event_loop()
t = threading.Thread(target=start_loop, args=(loop,))
t.start()
# 安全地从主线程调度回调
loop.call_soon_threadsafe(callback)
总结
这些调度方法为 asyncio
提供了灵活的方式来集成和管理回调,使得可以在适当的时间执行特定的函数。它们在处理定时任务、集成同步代码或从其他线程与异步代码交互时非常有用。
ThreadPollExecutor 和 asycio 完成阻塞 IO 请求
在 Python 的异步编程中,asyncio
与线程池(如 ThreadPoolExecutor
)的结合使用可以有效地处理阻塞 IO 操作,而不会阻塞整个异步事件循环。这种方法尤其适用于需要执行密集文件读写、网络请求或其他阻塞系统调用的场景。
ThreadPoolExecutor
ThreadPoolExecutor
是 concurrent.futures
模块的一部分,它管理一个线程池,可以用来执行并发的执行阻塞操作。通过将阻塞操作放在一个线程池中执行,主事件循环可以继续处理其他非阻塞操作。
结合使用 ThreadPoolExecutor 和 asyncio
以下是如何结合使用 ThreadPoolExecutor
和 asyncio
来处理阻塞 IO 操作的一个例子:
import asyncio
from concurrent.futures import ThreadPoolExecutor
import urllib.request
# 异步函数,用于在线程池中执行阻塞的 IO 操作
async def download_url(url):
print(f"开始下载: {url}")
# 在线程池中执行阻塞操作
loop = asyncio.get_running_loop()
with ThreadPoolExecutor() as executor:
# run_in_executor 参数:执行器,要运行的函数,函数的参数
result = await loop.run_in_executor(executor, urllib.request.urlopen, url)
data = result.read()
print(f"完成下载: {url}, 数据长度: {len(data)}")
return data
async def main():
urls = [
'http://example.com',
'http://example.org',
'http://example.net',
]
# 并发下载所有 URL
await asyncio.gather(*(download_url(url) for url in urls))
# 运行主函数
asyncio.run(main())
工作原理
- 创建异步函数:
download_url
是一个异步函数,它接受一个 URL,用于下载数据。 - 使用
ThreadPoolExecutor
:阻塞的urllib.request.urlopen
函数在ThreadPoolExecutor
中执行。这意味着它将在一个单独的线程中运行,而不会阻塞 asyncio 的事件循环。 - 等待结果:
await loop.run_in_executor(...)
等待线程池中的任务完成,并返回结果。这是一个异步等待,因此不会阻塞事件循环。 - 并发执行:
asyncio.gather
用于并发执行多个异步下载任务。
总结
通过结合使用 ThreadPoolExecutor
和 asyncio
,可以在异步应用中有效地处理阻塞 IO 操作,而不会影响程序的整体响应性。这种模式特别适合于需要处理大量独立的、耗时的阻塞操作的应用,如网络请求、文件操作等。
asyncio 模拟 http 请求
要在 asyncio
中模拟 HTTP 请求,我们可以使用 aiohttp
库,这是一个提供异步 HTTP 客户端和服务器的 Python 库。使用 aiohttp
进行 HTTP 请求是异步的,这意味着它不会阻塞事件循环,非常适合需要高性能的 IO 操作的应用程序。
首先,你需要安装 aiohttp
库,如果还没有安装的话,可以使用 pip 进行安装:
pip install aiohttp
示例:使用 aiohttp 发送异步 HTTP 请求
下面是一个使用 aiohttp
发送 GET 请求的简单示例:
import aiohttp
import asyncio
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
url = 'http://httpbin.org/get'
html = await fetch(session, url)
print(html)
asyncio.run(main())
代码解释
- 创建客户端会话:
aiohttp.ClientSession()
创建一个异步的 HTTP 客户端会话。使用async with
确保会话在结束时正确关闭。 - 发送 GET 请求:
session.get(url)
异步发送一个 GET 请求到指定的 URL。 - 处理响应:
response.text()
异步获取响应内容。async with
确保响应在处理完毕后正确关闭。 - 打印结果:打印出 HTTP 响应的内容。
进阶使用
如果你需要发送 POST 请求或添加请求头、处理 cookies 等,aiohttp
也支持这些功能。下面是一个发送 POST 请求的示例:
import aiohttp
import asyncio
async def post_data(session, url, data):
async with session.post(url, data=data) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
url = 'http://httpbin.org/post'
data = {'key': 'value'}
response_text = await post_data(session, url, data)
print(response_text)
asyncio.run(main())
这个示例展示了如何使用 aiohttp
发送 POST 请求。session.post(url, data=data)
用于发送 POST 请求,其中 data
是一个字典,包含你想要发送的数据。
总结
使用 aiohttp
和 asyncio
进行异步 HTTP 请求是处理网络请求的高效方式,特别是在需要高并发的场景下。通过异步操作,你的应用可以在等待网络响应时继续执行其他任务,从而提高整体性能和响应速度。
future 和 task
在 Python 的 asyncio
模块中,Future
和 Task
是两个核心的概念,它们都用于表示将来完成的操作,但它们在用途和行为上有一些区别。
Future
Future
对象是一个低层次的可等待对象,表示异步操作的最终结果。它通常由低级系统代码创建和使用,例如在库或框架的内部。Future
对象是一个抽象层,它使得可以在其上附加回调,并在操作完成时获取结果。
一个 Future
对象有以下关键状态:
- Pending: 初始状态,未完成的状态。
- Done: 操作完成,可以获取结果或异常。
- Cancelled: 操作被取消。
使用 Future
对象通常涉及以下步骤:
- 创建
Future
对象。 - 将
Future
对象传递给某个能够影响其状态的底层代码(例如,执行某种操作的库)。 - 等待
Future
对象的结果。
Task
Task
是 Future
的一个子类,它更高级且更具体,用于封装协程,是协程的运行容器。Task
对象用于调度协程的执行,让协程可以在事件循环中运行。当你创建一个 Task
对象时,你实际上将协程加入到了事件循环中,事件循环将负责执行它。
使用 Task
对象时,通常的步骤包括:
- 创建一个协程。
- 将协程封装到
Task
对象中(通常是通过asyncio.create_task()
)。 - 等待
Task
对象的结果。
示例代码
下面是一个示例,展示如何使用 Future
和 Task
:
import asyncio
async def set_after(fut, delay, value):
# 模拟延时操作
await asyncio.sleep(delay)
fut.set_result(value) # 设置 Future 对象的结果
async def main():
# 创建一个 Future 对象
fut = asyncio.Future()
# 等待 Future 对象设置结果
await asyncio.create_task(set_after(fut, 1, '...done'))
# 获取 Future 对象的结果
print(fut.result())
asyncio.run(main())
在这个示例中,set_after
是一个协程,它接受一个 Future
对象并在延迟后设置其结果。main
函数创建了一个 Future
对象,并使用 asyncio.create_task
创建了一个 Task
来运行 set_after
协程。
总结
- Future: 用于表示异步操作的最终结果,通常由底层库使用。
- Task: 是
Future
的子类,用于封装和调度协程的执行。
在实际应用中,当你使用 asyncio
编写异步代码时,通常会更多地与 Task
对象打交道,而 Future
对象更多地被库和框架内部使用。
asyncio同步和通信
在使用 asyncio
编写异步 Python 程序时,可能会遇到需要同步和通信的场景。asyncio
提供了多种机制来帮助协程之间进行同步和通信,包括事件、锁、信号量、条件变量和队列等。这些工具可以帮助你管理并发协程之间的交互,确保数据的一致性和操作的顺序性。
1. 事件(Event)
asyncio.Event
用于在协程之间发送信号。它的状态可以被设置为真或假。等待事件的协程将在事件被设置时继续执行。
import asyncio
async def waiter(event):
print('waiting for the event to be set')
await event.wait()
print('event is set')
async def main():
event = asyncio.Event()
# 将等待 event 的协程放入事件循环
asyncio.create_task(waiter(event))
# 模拟延迟,然后设置事件
await asyncio.sleep(1)
event.set()
asyncio.run(main())
2. 锁(Lock)
asyncio.Lock
可以用来保护共享资源。它类似于传统的线程锁,但是是为协程设计的,不会阻塞线程。
import asyncio
async def coro1(lock):
print('Coro1 waiting for the lock')
async with lock:
print('Coro1 acquired lock')
await asyncio.sleep(2)
print('Coro1 released lock')
async def coro2(lock):
print('Coro2 waiting for the lock')
async with lock:
print('Coro2 acquired lock')
await asyncio.sleep(2)
print('Coro2 released lock')
async def main():
lock = asyncio.Lock()
await asyncio.gather(coro1(lock), coro2(lock))
asyncio.run(main())
3. 信号量(Semaphore)
asyncio.Semaphore
用于限制同时访问某个资源的协程数量。
import asyncio
async def worker(semaphore, num):
async with semaphore:
print(f'Worker {num} is working')
await asyncio.sleep(2)
async def main():
semaphore = asyncio.Semaphore(2) # 同时只允许2个协程工作
await asyncio.gather(*(worker(semaphore, i) for i in range(4)))
asyncio.run(main())
4. 条件变量(Condition)
asyncio.Condition
用于协程间的复杂同步,允许协程等待或通知特定条件的变更。
import asyncio
async def consumer(condition, n):
async with condition:
print(f'consumer {n} is waiting')
await condition.wait()
print(f'consumer {n} triggered')
async def producer(condition):
await asyncio.sleep(2)
async with condition:
print('producer is notifying')
condition.notify_all()
async def main():
condition = asyncio.Condition()
consumers = [asyncio.create_task(consumer(condition, i)) for i in range(3)]
await asyncio.sleep(0.1) # 确保消费者先运行
await producer(condition)
await asyncio.gather(*consumers)
asyncio.run(main())
5. 队列(Queue)
asyncio.Queue
用于协程间的消息传递。它是线程安全的,适合用于生产者-消费者问题。
import asyncio
async def producer(queue):
for i in range(5):
await queue.put(i)
print(f'produced {i}')
await asyncio.sleep(1)
async def consumer(queue):
while True:
item = await queue.get()
print(f'consumed {item}')
queue.task_done()
async def main():
queue = asyncio.Queue()
producer_task = asyncio.create_task(producer(queue))
consumer_task = asyncio.create_task(consumer(queue))
await asyncio.sleep(10)
consumer_task.cancel()
asyncio.run(main())
总结
通过使用 asyncio
提供的同步和通信机制,你可以有效地管理和协调协程之间的交互,确保程序的正确性和效率。这些工具可以应对不同的并发编程需求,帮助你构建健壮的异步应用。
aiohttp实现高并发爬虫
使用 aiohttp
库实现高并发的爬虫是 Python 异步编程的一个常见应用。aiohttp
是一个提供异步 HTTP 客户端和服务器功能的库,它基于 asyncio
,因此可以处理大量并发 HTTP 请求,非常适合用于构建高效的网络爬虫。
步骤概述
- 安装 aiohttp:首先需要安装
aiohttp
库。 - 创建异步 HTTP 会话:使用
aiohttp.ClientSession()
创建会话。 - 发起异步请求:使用会话对象发起 GET 或 POST 请求。
- 处理响应:对返回的响应进行处理,如解析 HTML。
- 并发控制:使用异步信号量 (
asyncio.Semaphore
) 控制并发量,防止过多请求压垮服务器或触发反爬机制。 - 异常处理:正确处理网络请求过程中可能出现的异常。
- 数据存储:将爬取的数据存储到文件或数据库中。
示例代码
下面是一个简单的示例,展示如何使用 aiohttp
实现一个基本的高并发爬虫:
import aiohttp
import asyncio
from bs4 import BeautifulSoup
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def parse(html):
soup = BeautifulSoup(html, 'html.parser')
titles = soup.find_all('h2')
for title in titles:
print(title.get_text())
async def main(urls, max_concurrency):
# 控制并发数量
semaphore = asyncio.Semaphore(max_concurrency)
async with aiohttp.ClientSession() as session:
async def bounded_fetch(url):
async with semaphore:
html = await fetch(session, url)
await parse(html)
tasks = [bounded_fetch(url) for url in urls]
await asyncio.gather(*tasks)
urls = [
'https://example.com/page1',
'https://example.com/page2',
# 更多 URL
]
# 设置最大并发数
max_concurrency = 10
asyncio.run(main(urls, max_concurrency))
重要注意事项
- 合理设置并发数:并发数不宜过高,以免给目标服务器带来过大压力,也可能导致被封IP。
- 遵守 Robots 协议:在进行网络爬虫开发时,应该遵守目标网站的
robots.txt
文件规定,尊重网站的爬虫政策。 - 异常处理:网络请求可能会因为各种原因失败,如超时、连接错误等,应适当处理这些异常。
- 异步安全:确保你的代码在异步环境中是安全的,特别是在操作共享资源如数据库时。
通过这种方式,你可以有效地利用 aiohttp
和 asyncio
的异步特性来构建一个高效且强大的网络爬虫。