02.08-Agent学习记录-并发与并行

上一篇介绍了异步编程:程序等待日历服务时,可以通过合适的调度,让其他已经能够继续的工作得到执行机会。接下来还需要区分两个经常同时出现的词:并发和并行。

舒翱移动端 / 全栈 / Agent 应用

正文

假设会议助手需要读取两份相互独立的日历,再比较其中的项目例会。程序可以先读完第一份,再开始第二份;也可以在第一份等待响应时,发出第二份请求。

这两种安排得到的资料可能相同,但推进过程不同。

并发关注多项工作在同一段时间内怎样被组织和推进;并行则强调某个时刻,确实有多个执行单元在同时做工作。

一台只有一个执行核心的机器,也可以交错推进多个任务:先处理第一项的一部分,再切换到第二项。它能够表现出并发,但在那个核心上,并没有同时执行两段计算。

如果两个核心分别执行两段计算,就存在计算层面的并行。两个远程日历服务同时处理请求,也可能在服务端发生并行,而本地程序仍然只用一个事件循环接收结果。

因此,讨论这两个词时,最好说明观察的是哪一部分:本地程序、多个服务,还是整项任务。它们可以同时存在,不是互相排斥的选择。

先看顺序处理。第一份日历没有读完,第二份就不开始。这种安排容易理解,适合有先后依赖的工作,但无法主动利用两次独立请求之间的等待重叠。

再看异步并发。程序发出第一项请求,在等待时安排第二项。两份结果分别回来,再进入汇总。在常见的单线程事件循环中,本地回调与任务仍然轮流执行,等待外部 I/O 的时间则可以重叠。事件循环中的任务调度

这里减少的可能是整项任务的等待,而不是每个服务自身的处理量。若两个请求最终都要由同一个忙碌服务依次处理,本地同时发出请求,也未必得到预期的提速。

因此,并发不是“把任务数量调大就一定更快”。需要先判断当前主要在等什么,以及额外任务会不会争用同一个资源。

常见的一种区分,是 I/O 密集与计算密集。

如果程序主要在等待网络响应、文件读写或外部服务,属于偏 I/O 的工作。等待时允许其他独立操作推进,可能帮助减少空闲。

如果程序主要在进行大量数值计算或复杂的数据处理,计算资源本身可能是主要限制。只让一个事件循环更频繁地切换,并不会让同一份计算凭空减少。

这时,就会接触线程和进程。

线程可以先理解为进程内的一条执行线。一个进程中的多个线程,通常共享同一进程的内存资源,因此可以方便地接触共同数据,也需要协调对这些数据的修改。

线程可能由操作系统调度,在多个核心上获得执行机会。但语言运行时、使用的库和工作类型,会影响实际能否同时执行相关计算,不能只看线程数量。

对于常见的启用全局解释器锁,也就是 GIL 的 CPython 构建,同一个解释器中通常同一时刻只有一个线程执行 Python 字节码。因此,给纯 Python 计算增加线程,并不等于自动获得多核计算加速。CPython 线程与 GIL 的限制

这也不是说 Python 线程永远不能发挥作用。等待 I/O 的工作、会释放 GIL 的原生扩展,以及不同运行时环境,都需要具体分析。

Python 还提供可选的自由线程构建,可以在相应条件下关闭 GIL 的限制。是否使用这种构建、扩展是否兼容,以及实际运行时状态,都需要确认,不能把这种能力当作所有本机环境已经启用的事实。自由线程构建的支持与条件

进程则是另一种组织方式。不同进程通常拥有各自的地址空间和运行状态,可以在不同核心上处理独立计算。Python 的多进程能力,也可以用于绕开同一解释器内的 GIL 限制。

但进程之间传递数据、启动工作和汇总结果,都有成本。有时还需要序列化数据;需要共享内存时,也要明确设计相应机制。不能把一个进程里的普通列表,直接当作所有进程共同修改的可靠状态。多进程的执行与通信

平台和启动方式也会影响程序行为,所以真实项目应当使用适合环境的成熟执行机制。本篇先理解职责,不需要为了学习概念,在电脑上启动大量工作进程。

异步任务、线程和进程,因而是在不同层次解决执行安排的问题。一项应用也可以组合使用它们,但每增加一层安排,就多了一些结果交接、错误处理和资源管理工作。

回到会议助手,并不是所有动作都适合一起做。

读取两份已经明确相关的日历,可能相互独立;根据第一份查询找到会议编号,再读取对应详情,则存在依赖。尚未确定会议,就提前生成并保存最终通知,可能导致后续工作建立在错误资料上。

因此,在考虑并发之前,可以先标明哪些结果由哪些步骤使用。能够重叠的,是没有被这些依赖关系限制、并且符合资源与权限约定的部分。

汇总结果时,也要保持对应关系。第二份日历先返回,并不改变它属于哪一份请求。结果应当带有清楚的任务或记录标识,不能仅凭返回先后,把资料放进错误的位置。

对返回顺序的处理,也要阅读具体接口约定。有的收集方式按输入顺序交付,有的按完成顺序提供。程序需要明确自己正在使用哪种方式,不能把两者混用。

多项工作同时推进以后,还会出现竞争条件。

假设两个任务都准备保存同一份通知。它们先检查“文件还不存在”,然后分别继续写入。即使两项检查都曾得到真实结果,也不保证后来只有一个任务创建文件。

问题在于,检查和实际操作之间,其他任务改变了环境。单线程异步程序也可能在等待点之间遇到这种交错,不是只有多个计算核心才会出现竞争。

应对方式要结合具体操作。可以使用排他创建、合适的同步机制,或让服务通过唯一约束与幂等机制控制重复结果。只在模型提示中写“不要重复”,不能代替执行层的保证。

并发还需要边界。例如,同一时刻最多允许多少个日历请求,其余任务在哪里排队,等待过久怎样处理,失败以后由谁取消或收集结果。

限制同时在途的数量,关注并发规模;限制一段时间内发出多少请求,则关注调用速率。两者有联系,但并不相同。即使同时请求不多,完成得很快,也可能在短时间内产生大量调用。

如果任务接收得比处理得快,队列就可能越来越长。应用需要按能力调节接收和处理,避免把全部待办都变成内存中的活动任务。这也是后面“限流与背压”要解决的问题。

发生错误时,也要明确影响范围。一份日历失败,是让整个任务结束,还是保留另一份成功结果并说明缺失?某个任务取消后,其他任务是否还应继续?这些都属于执行设计,不会由“并发”这个名称自动决定。

资源使用同样会影响实际收益。更多任务可能同时占用连接、内存和外部服务额度;更多进程可能带来额外内存与通信成本。增加数量之前,需要确认当前限制在哪里,并保留前后可比较的测量条件。

结果正确性也应当一起比较。一个方案更快,但经常混用会议记录、重复创建草稿,不能仅凭速度称为改进。相反,多做必要核对造成的等待,也应该与避免错误的收益一起评价。

初学时,可以先画出两三项工作之间的依赖:哪些必须先做,哪些可以交错等待,哪些计算确实需要同时执行。再选择合适的执行方式,而不是先决定“必须上多线程”。

下一篇进入 02.09:异常处理与资源清理,继续看其中某一步失败时,程序怎样交代错误、结束有关工作,并释放本次使用的资源。


当前学习编号:02.08 并发与并行。
下一学习编号:02.09 异常处理与资源清理。后续还有其他学习编号。

关联分支:执行与依赖对应 02.07 异步编程、16.11 多工具依赖与并行边界、18.03 任务拆解与依赖关系、18.06 顺序、并行与扇出汇总、20.07 任务依赖与并发调度。

一致性与资源对应 16.10 工具幂等与重复执行、21.17 数据一致性与并发控制、24.08 限流与背压、25.07 并发限制、队列与扩缩容、25.11 吞吐量与性能测量。线程、进程、事件循环、GIL、竞争条件、同步与工作池,是本编号可继续下钻的支系知识。

系列阅读

本篇学习编号:02.08 并发与并行。

上一篇: 02.07 异步编程 。

下一篇: 02.09 异常处理与资源清理 。

延伸阅读

返回全部文章

搜索项目和文章

输入关键词,查找项目或文章。

使用 Tab 选择结果,按 Esc 关闭。