泛泛地谈一些心得和方法论的东西,外加一些通用的宽泛知识。

存在 AIGC,但是它们都经过验证。

并行任务处理

关于此内容的 C++ 方式,可翻阅本博客的其他文章。

如何监控和管理子进程

在 Python 中,subprocess 模块是执行外部命令和管理子进程的标准库。

  1. 启动进程:

    • subprocess.run(): 这是一个高级函数,它会启动一个命令,等待它完成,然后返回一个包含其退出码、标准输出和标准错误的 CompletedProcess 对象。这是一个阻塞操作,在子进程结束前,你的 Python 脚本会一直等待。
    • subprocess.Popen(): 这是一个更底层的接口,它会立即启动一个命令而不会等待它完成。它返回一个 Popen 对象,你可以用这个对象来与进程交互,比如等待它 (.wait())、检查它的状态 (.poll())、或者读取它的输出流。这是实现并行处理的基础。
  2. 监控进程是否结束:

    • process.wait(): 这是一个阻塞方法。调用它会暂停你的脚本,直到这个特定的子进程结束。
    • process.poll(): 这是一个非阻塞方法。调用它会立即返回进程的状态:如果进程仍在运行,返回 None;如果进程已经结束,返回它的退出码(通常 0 表示成功)。你可以在一个循环中反复调用 .poll() 来检查多个进程的状态,而不会阻塞主程序。
  3. 获取进程的输出:

    • 在创建 Popen 对象时,你可以将 stdoutstderr 参数设置为 subprocess.PIPE。这会将子进程的标准输出和标准错误重定向到管道,你的 Python 脚本可以像读取文件一样从中读取内容。
    • process.communicate(): 这个方法会等待进程结束,然后一次性读取并返回所有的 stdoutstderr 内容。这是最简单、最安全的读取输出的方式,因为它能避免管道被填满导致的死锁问题。

并发写入文件的互斥问题分析

多个进程同时写入一个文件是否需要加锁?

  • 答案是:需要。 如果没有同步机制,多个进程可能在几乎完全相同的时间点尝试写入文件,导致:

    • 内容交错
    • 数据丢失: 在某些文件系统和操作下,并发写入甚至可能导致部分数据丢失。
  • 解决方案: 使用锁 (Lock)。在 Python 的 multiprocessing 模块中,可以创建一个 Lock 对象。任何进程在写入文件前,必须先获取 (acquire()) 这个锁。当它持有锁时,其他任何尝试获取锁的进程都会被阻塞,直到该进程释放 (release()) 锁。这确保了在任何时刻,只有一个进程能够执行写入文件的代码块。

使用 multiprocessing.Pool

对于你描述的“固定数量的进程池”任务,multiprocessing.Pool 是最优雅、最高效的解决方案。它为你封装了所有复杂的进程管理逻辑:

  • 你创建一个 Pool 并指定工作进程的数量 (use_core)。
  • 你给它一个任务列表(我们要运行的命令)。
  • Pool 会自动启动工作进程,从任务列表中取出任务并执行。
  • 它会自动维护一个正在运行的进程队列,一旦有进程完成,它会立刻从任务列表中取下一个任务来执行,直到所有任务都完成。

集群并行作业调度

我的同事:

你们跑 某个命令 的时候,是直接敲的吗?我发现用 bsub -q short -Is 某个命令 会快一些,也不影响工作站其他任务。

解释

简单来说,同事的建议是利用集群管理系统来运行实验,而不是直接在登录节点(你登录的服务器)上运行。这是一种非常好的实践,特别是在多人共享的服务器或计算集群环境中。

bsub 是一个命令,它属于一个叫做 LSF (Load Sharing Facility) 的作业调度系统。你可以把它想象成一个“任务分发官”。

在很多大学、研究机构和公司里,计算资源(服务器/工作站)是被很多人共享的。为了公平、高效地分配计算资源,管理员会安装 LSF 这样的系统。你不能直接登录到一台机器上就占用所有 CPU 和内存跑程序,而是要把你的任务“提交”给 LSF,LSF 会根据当前系统负载情况,找一台合适的、空闲的机器去执行你的任务。

命令拆解:bsub -q short -Is 某个命令

  • bsub: 这是核心命令,意思是“我要提交一个作业(job)”。

  • -q short:

    • -q 代表 --queue (队列)。
    • short 是队列的名字。
    • 管理员通常会根据任务的特性(比如预计运行时长、需要的资源多少)创建不同的队列。short 队列通常意味着为“短时间运行”的任务准备的,可能会有更高的优先级或者能更快地被调度。
  • -Is: 这是两个参数的组合,非常关键。

    • -I: 代表 Interactive (交互式)。这个参数告诉 LSF,这不是一个提交后就不用管的后台任务。我需要像在普通终端里一样,能实时看到程序的输出,并且程序结束后马上返回到我的命令行。如果没有 -Ibsub 会把任务提交到后台,你只能通过其他命令或查看日志文件来了解运行情况。
    • -s: 代表 pseudo-terminal (伪终端)。这个参数配合 -I 使用,可以确保你的交互式任务拥有一个合适的终端环境,这样像 ns-3 运行时的进度条、颜色输出等都能正常显示。
  • 某个命令: 这就是你要 LSF 系统为你执行的真正命令。

所以,整个命令的白话翻译就是:

“你好 LSF 系统,请帮我启动一个交互式的任务,把它放到 short 这个队列里去排队。我希望这个任务能有个终端让我看实时输出。你要执行的命令是 某个命令。”

为什么这样做更好?

“会快一些”。

这可能有两方面原因:

  • 分配到更强的节点:当你直接在登录服务器上运行命令时,你用的是这台登录服务器的 CPU 和内存。登录服务器通常是给用户登录、编辑代码、管理文件用的,计算性能不一定强,而且可能有很多人在同时使用,资源紧张。而当你用 bsub 提交任务时,LSF 会从整个集群里为你挑选一个专门用于计算的节点(Compute Node)。这些计算节点通常配置更高(更多的 CPU核心、更大的内存),并且当前可能更空闲,所以你的程序自然跑得更快。
  • 避免资源争抢:登录服务器上可能运行着很多其他人的小进程,甚至是有人也在上面直接跑计算任务。你的程序需要和它们“抢”CPU 时间。而计算节点由 LSF 管理,会确保分配给你的资源(比如一个 CPU 核心)在任务期间是专属的,大大减少了干扰。

“不影响工作站其他任务”。

这是在共享环境中最重要的一点,也是体现专业素养的地方。

  • 直接运行的坏处:如果你直接在登录服务器上运行一个计算密集型的仿真,它可能会瞬间吃掉大量的 CPU 和内存。这会导致:
    • 服务器卡顿:所有正在这台服务器上工作的其他人(包括你自己)都会感觉到明显的卡顿,连敲一个 ls 命令都可能要等半天。
    • 系统不稳定:如果你的程序内存占用过大,可能会导致服务器因为内存耗尽而崩溃或杀死其他人的进程。
  • 使用 bsub 的好处:你将计算压力从“公共的”登录服务器转移到了“专业的”计算节点上。登录服务器只负责处理你提交任务的这个请求,本身几乎没有负载。繁重的计算发生在后端专门的机器上,完全不会影响到登录服务器的响应速度和其他用户的正常工作。

总结

你的同事是在告诉你一个在共享计算环境中正确、高效且礼貌的工作方式。

操作方式 直接运行命令 通过 LSF 运行 (bsub ...)
运行地点 你登录的那台服务器 (登录节点) LSF 系统分配的某台服务器 (计算节点)
资源 与服务器上所有人共享、抢占 LSF 分配的专属、更优的资源
对他人影响 。可能造成服务器卡顿,影响他人 。不占用登录节点资源,不影响他人
效率 可能较慢,受他人影响大 通常更快,资源独占且配置更好
推荐度 不推荐 (在共享环境中) 强烈推荐