17

我需要将一个文件从一个位置复制到另一个位置,如果该文件已经存在于目的地(没有覆盖),我需要抛出一个异常(或至少以某种方式识别)。

我可以先用 os.path.exists() 检查,但在检查和复制之间的短时间内不能创建文件是非常重要的。

有没有一种内置的方法可以做到这一点,或者有没有办法将一个动作定义为原子的?

4

2 回答 2

18

事实上,只要所有参与者都以相同的方式执行此操作,就可以自动安全地执行此操作。它是对无锁打鼹鼠算法的改编,而且并非完全无关紧要,因此请随意选择“否”作为一般答案;)

该怎么办

  1. 检查文件是否已经存在。如果有就停止。
  2. 生成唯一 ID
  3. 使用临时名称将源文件复制到目标文件夹,例如<target>.<UUID>.tmp.
  4. 重命名†</sup> 副本<target>-<UUID>.mole.tmp
  5. 查找与该模式匹配的任何其他文件 <target>-*.mole.tmp
    • 如果他们的 UUID 比你的大,尝试删除它。(如果它消失了,请不要担心。)
    • 如果他们的 UUID 比你的少,尝试删除你自己的。(同样,如果它消失了,请不要担心。)从现在开始,将他们的 UUID 视为您自己的。
  6. 再次检查目标文件是否已存在。如果是这样,请尝试删除您的临时文件。(如果它消失了,请不要担心。请记住,您的 UUID 可能在第 5 步中已更改。)
  7. 如果您尚未尝试在步骤 6 中将其删除,请尝试将临时文件重命名为其最终名称<target>. (如果它消失了,请不要担心,只需跳回第 5 步。)

你完成了!

这个怎么运作

想象一下每个候选源文件都是一个从洞里出来的鼹鼠。到一半时,它会停下来,将所有竞争的鼹鼠打回地面,然后检查是否没有其他鼹鼠完全出现。如果你在脑海中思考这个问题,你应该会发现只有一颗痣会完全摆脱它。为了防止这个系统活锁,我们添加了一个总排序,哪个鼹鼠可以敲哪个。砰! 博士论文 锁算法

†</sup> 第 4 步可能看起来没有必要——为什么不首先使用该名称?但是,另一个进程可能会在第 5 步中“采用”您的 摩尔 文件,并使其在第 7 步中胜出,因此您不要仍然写出内容,这一点非常重要!同一文件系统上的重命名是原子的,因此第 4 步是安全的。

于 2015-01-22T14:13:46.550 回答
13

没有办法做到这一点;文件复制操作永远不是原子的,也没有办法制作它们。

但是您可以使用随机的临时名称编写文件,然后重命名它。重命名操作必须是原子的。如果文件已经存在,重命名将失败,您将收到错误消息。

[EDIT2] rename()只有在同一个文件系统中执行时才是原子的。安全的方法是在与目标相同的文件夹中创建新文件。

[编辑]重命名是否总是原子的以及关于覆盖行为的讨论很多。所以我挖了一些资源。

在 Linux 上,如果目标存在并且源和目标都是文件,那么目标会被静默覆盖(​​手册页)。所以我错了。

但是rename(2)如果出现问题,仍然保证原始文件或新文件仍然有效,因此操作是原子的,因为它不会损坏数据。从某种意义上说,它不是原子的,它可以防止两个进程同时进行相同的重命名,并且您可以预测结果。一个会赢,但你不知道哪个。

在 Windows 上,如果另一个进程当前正在写入文件,如果您尝试打开它进行写入,则会出现错误,因此这里是 Windows 的一个优势。

如果您的计算机在将操作写入磁盘时崩溃,文件系统的实现将决定有多少数据被损坏。应用程序对此无能为力所以停止抱怨了:-)

也没有其他方法可以更好地工作,甚至与此方法一样好。

您可以改用文件锁定。但这只会使一切变得更加复杂并且不会产生额外的优势(除了更复杂,有些人出于某种原因确实认为这是一个巨大的优势)。当您的文件位于网络驱动器上时,您会添加很多不错的角落案例。

如果文件已经存在,您可以使用会使函数失败open(2)的模式。O_CREAT但这不会阻止第二个过程删除文件并编写自己的副本。

或者您可以创建一个锁定目录,因为创建目录也必须是原子的。但这也不会让你买太多。您必须自己编写锁定代码,并且绝对 100% 确保您真的,真的总是在发生灾难时删除锁定目录 - 这是您做不到的。

于 2012-07-23T14:46:04.717 回答