什么是 asyncio?
Scrapeless Proxies 通过可配置的代理连接,将来自你自己的 HTTP 客户端的请求转发。
asyncio 是 Python 标准库中用于通过异步函数和事件循环协调并发工作的框架。当程序需要花时间等待网络响应、子进程或其他提供异步接口的操作时,它非常有用。处于等待中的任务可以挂起,从而让其他就绪任务继续执行。
一个采集任务可以说明这种区别。抓取页面需要等待远程服务器;解析大型文档则需要执行本地指令。asyncio 可以在彼此独立的请求之间并行重叠第一类活动,但它不会自动让第二类活动在多个 CPU 核心上执行。理解这条边界有助于你判断异步设计是否会改善你的应用。
asyncio 实际上管理什么?
asyncio 负责调度和管理异步操作的生命周期,而单独的客户端库通常实现应用协议。 Python 异步 I/O 接口 包含用于任务、同步、网络操作以及子进程管理的工具。HTTP 客户端可以利用这些工具在不使用阻塞式读取占用事件循环线程的情况下等待响应。
异步函数用 async def 声明。调用它会创建一个协程对象;仅仅调用并不会完成函数体。协程必须被 await 或被调度执行。这也是为什么给函数名或声明加上 async 并不会把现有同步代码自动变成并发工作的原因。
在实际程序中有三个反复出现的概念。协程描述的是一个可以挂起的操作。任务负责调度协程并跟踪它最终的结果。事件循环根据 I/O 就绪情况协调就绪的回调和任务。Future 表示一个最终结果,并且经常出现在较底层的 API 中;应用代码通常直接处理协程和任务而不是 Future。
事件循环如何共享执行?
事件循环在其线程内以协作的方式执行工作。正在运行的任务会保持控制权,直到它完成或在需要等待的操作处挂起。当网络数据可用时,事件循环可以恢复相关任务。这种调度模型不同于假设每一行异步代码都在同一时刻运行。
await 关键字表达对一个可等待结果的依赖。这并不是一个无条件的“暂停”指令。如果被等待的操作可以立刻完成,执行可能会继续而不会让其他任务获得轮转。一个在挂起点之间执行大量计算的循环,即使身处异步函数中,也仍然可能延迟不相关操作的执行。
考虑一个小型目录收集器。一个任务等待商品页,另一个任务等待分类页,还有一个写入器等待已验证的记录。每个任务在其依赖就绪时都可以取得进展。如果商品解析器在事件循环线程上执行了一段长时间的同步计算,那么分类任务在这段计算期间便无法恢复。网络可能早已返回了响应,但应用仍然需要等到有机会处理它。
顺序等待与并发任务
依次 await 彼此独立的操作会让它们保持顺序,除非它们已经被调度。如果一个函数在创建第二个抓取操作之前先等待第一个抓取完成,那么第二个操作就无法与第一个重叠。先创建多个任务再等待它们的结果,可以为调度器提供可以独立协调的工作。
任务的归属与任务的创建同样重要。必须有人负责观察结果、报告异常,并在线程父操作结束时终止这些工作。未被跟踪的后台任务可能会让调用者在仍有有用工作在运行或已在可见控制流之外失败的情况下收到成功消息。
Python 的 协程和任务生命周期 包含用于结构化归属的 TaskGroup。任务组在离开其上下文时会等待子任务。如果某个子任务因除取消以外的异常失败,任务组会取消剩余子任务,并通过其文档规定的异常处理机制报告失败。这种行为适用于一组成败都归属于同一父作业的操作。
有些应用则需要彼此独立的结果。一个检查若干互不相关公共页面的报告,可能会保留成功的观测并单独记录失败的观测。在选择协调原语之前先决定这种策略。否则,应用可能会意外丢弃有用结果,或者把不完整的集合当作完整集合呈现。
为什么并发需要容量限制
并发限制让应用保持在其客户端、目标端以及存储的承载能力之内。为每个发现的 URL 创建一个任务,可能会在第一个响应到达之前就耗尽内存。即使套接字数量受到限制,排队的任务对象、响应缓冲区和待处理记录也仍然可能持续增长。
信号量可以限制进入受保护区域的操作数量。其作用范围应覆盖你真正想限制的资源。如果它只覆盖连接建立但在响应被消费前就释放,体积巨大的响应主体仍然可能无限制地积累。当你的意图是以整个请求生命周期为单位时,就应把完整的请求生命周期置于限制之内。
有界队列在阶段之间增加了背压。当队列已满时,生产者会等待,而不是无限地发现新工作。 asyncio 队列契约 将从队列中取出项目与将其处理标记为完成区分开来。这在某个工作线程必须先验证并存储记录,才能将生产者的工作视为完成时非常有用。
阻塞式代码导致问题的情形
阻塞代码会在阻塞操作运行期间阻止事件循环线程为其他任务提供服务。常见示例包括同步 HTTP 调用、缓慢的文件操作以及耗时的解析或转换过程。此类调用外面的异步包装并不会改变该调用本身的行为。
当库支持本机异步客户端时,请使用本机异步客户端。对于现有的阻塞 I/O 函数,可以使用 asyncio.to_thread 将该函数移动到另一个线程,以便事件循环线程保持可用。这是针对某个特定操作的桥接措施,并不意味着整个程序因此变成异步或线程安全。
CPU 密集型工作值得单独考虑。对于大量计算,根据所使用的 Python 运行时和库,基于进程的 worker 可能更合适。需要考虑将数据传入和传出该 worker 的成本。将一个庞大的解析对象跨进程边界发送,可能会抹消分布计算所带来的收益。
The asyncio 开发指南 描述了调试工具和阻塞代码相关的问题。在调查过程中,将任务持续时间和事件循环的响应能力与远程响应时间分开记录。一个缓慢的端到端操作可能是在等待应用程序,而不是在等待网站。
取消、截止日期和资源清理
取消操作会在取消机会点请求异步工作停止;它并不能证明远程操作从未被接收。如果客户端提交了写入请求,然后在等待期间取消,该写入很可能已经在服务器端被应用。工作流必须体现这种不确定性,而不是将取消视为回滚。
对任务拥有的资源使用清理路径。异步上下文管理器可以在其作用域结束时关闭客户端或响应流。即使任务通过取消退出,finally 块也可以释放应用程序资源。避免在没有明确所有权策略的情况下吞掉取消信号,因为结构化任务管理依赖于其被正确传播。
网络超时和工作流截止时间回答的是不同的问题。客户端超时可以限定等待某个网络阶段的时间。工作流截止时间可以限定更大操作(包括排队和处理)的生命周期。追踪是在哪个环节触及了截止时间,这样运维人员就能区分是本地队列过载,还是目标端响应缓慢。
asyncio 如何适配 Web 数据管道
asyncio 适用于其依赖项提供异步操作的流水线的编排层。一个有用的布局是将发现、获取、验证和存储分离开来。每个阶段都接受一个明确的输入,并产生一个具有足够标识度的结果,以便将其与原始条目关联起来。
对于一个假设的目录检查,需保留请求的 URL、最终 URL、观测时间、响应状态以及解析结果。一个返回了 HTML 但没有包含预期产品字段的页面应被记录为无效观测。更快的调度并不会让该响应变得有用。空的抽取结果与有效的空集合是不同的结果。
在预期的生命周期内重用 HTTP 客户端,并有意识地保留会话状态。 HTTPX 代理配置 讨论提供了相关的客户端上下文,而当前 无痕代理连接模型 描述网络层。代理路由和任务调度仍然是独立的控制;添加代理不会限制应用程序的并发性。
在选择 无爬取代理,根据客户设计评估所需的地理位置和连接连续性。请咨询 无爬取定价 在估算批次之前,先为所选产品完成上述操作。基于实际的请求和响应工作负载进行估算,包括未通过验证的内容。
什么时候同步设计更好?
当任务几乎没有可以重叠的独立 I/O 时,同步设计往往更易于维护。一个获取单个文档并立即解析的短指令,可能几乎无法从事件循环的协调中获益。现有的同步依赖关系也会使部分代码转换比原始代码更加复杂。
当你能够识别出有意义的等待、可以分离的独立工作并能管理其生命周期时,选择使用 asyncio。当操作本质上是顺序的,或是计算占据了主要运行时间时,则选择更简单的设计。决策应依据工作负载特征,而不是基于“异步代码更快”这一泛化假设。
结论
asyncio 通过显式挂起和任务调度来协调等待中的工作。从一个有界的聚合作业开始,为每个任务指定一个所有者,并将网络等待与本地处理分离开来。在增加工作负载之前,先审查关闭行为和结果校验;这些选择决定了并发是否能产生可信的数据。
连接你的并发收集器
将有界的 Python 集合工作流与其所需的代理路由和会话策略进行配对。
立即注册即可获得 5 美元的免费额度 — 无需信用卡.
领取您的 $5 额度 →常见问题解答
asyncio 是 Python 的一部分吗?
asyncio 是 Python 标准库的一部分。第三方 HTTP 或数据库客户端可能基于它构建,但它们是具有自己连接行为和配置的独立包。
asyncio 会在多个 CPU 内核上运行 Python 代码吗?
仅靠 asyncio 的调度并不能将 Python 的执行分布到多个 CPU 核心上。事件循环会在其所属的线程中运行任务,而对于需要其他执行上下文的工作,可以使用单独的执行器或进程来完成。
每个 await 都会切换到另一个任务吗?
await 表达式并不总是切换到另一个任务。一个立即完成的 awaitable 可能会在当前执行路径中继续执行,而不会发生挂起。
asyncio 能控制代理轮换吗?
asyncio 可以调度使用代理的操作,但路由和轮换行为由客户端和代理服务提供商决定。请将会话策略与应用程序的并发控制分离开来。
为什么异步应用程序仍然会无响应?
当阻塞调用或长时间计算占用其事件循环线程时,异步应用程序也会变得无响应。在把每一次延迟都归咎于远程服务之前,请先检查本地处理、队列增长情况以及连接数限制。