Python中创建文件夹的竞争条件

17 投票
5 回答
7627 浏览
提问于 2025-04-15 15:10

我有一个用来缓存的urllib2模块,但它偶尔会因为以下代码崩溃:

if not os.path.exists(self.cache_location):
    os.mkdir(self.cache_location)

问题是,当第二行代码执行时,文件夹可能已经存在,这样就会出错:

  File ".../cache.py", line 103, in __init__
    os.mkdir(self.cache_location)
OSError: [Errno 17] File exists: '/tmp/examplecachedir/'

这是因为这个脚本会被我无法控制的第三方代码同时多次启动。

在我尝试修复这个错误之前的代码可以在这里找到,GitHub上

我不能使用tempfile.mkstemp,因为它通过使用随机命名的目录来解决竞争条件,这样就失去了缓存的意义。

我不想简单地忽略这个错误,因为如果文件夹的名字已经存在为一个文件(这是一个不同的错误),同样会出现Errno 17错误,例如:

$ touch blah
$ python
>>> import os
>>> os.mkdir("blah")
Traceback (most recent call last):
  File "", line 1, in 
OSError: [Errno 17] File exists: 'blah'
>>>

我不能使用threading.RLock,因为这个代码是从多个进程中调用的。

所以,我尝试写一个简单的基于文件的锁(这个版本可以在这里找到),但这有个问题:它在上一级目录创建锁文件,所以对于/tmp/example/来说,它会创建/tmp/example.lock,如果你把/tmp/作为缓存目录使用,就会出错(因为它试图创建/tmp.lock)。

总之,我需要将urllib2的响应缓存到磁盘。为此,我需要以多进程安全的方式访问一个已知的目录(如果需要的话创建它)。这个方法需要在OS X、Linux和Windows上都能工作。

有什么想法吗?我能想到的唯一替代方案是用SQLite3存储重写缓存模块,而不是使用文件。

5 个回答

10

我最后写出来的代码是:

import os
import errno

folder_location = "/tmp/example_dir"

try:
    os.mkdir(folder_location)
except OSError as e:
    if e.errno == errno.EEXIST and os.path.isdir(folder_location):
        # File exists, and it's a directory,
        # another process beat us to creating this dir, that's OK.
        pass
    else:
        # Our target dir exists as a file, or different error,
        # reraise the error!
        raise
11

与其使用

if not os.path.exists(self.cache_location):
    os.mkdir(self.cache_location)

你可以这样做

try:
    os.makedirs(self.cache_location)
except OSError:
    pass

这样做的结果是功能是一样的。

免责声明:我不知道这样做在Python中是否合适。


使用 SQLite3 可能有点过于复杂,但它能为你的需求增加很多功能和灵活性。

如果你需要频繁地进行“选择”、同时插入和过滤数据,使用 SQLite3 是个不错的主意,因为它不会比简单的文件增加太多复杂性(甚至可以说它能减少复杂性)。


重新阅读你的问题(和评论)后,我能更好地理解你的问题。

有没有可能一个文件也会产生相同的竞争条件?

如果文件足够小,我会这样做:

if not os.path.isfile(self.cache_location):
    try:
        os.makedirs(self.cache_location)
    except OSError:
        pass

另外,看看你的代码,我会把

else:
    # Our target dir is already a file, or different error,
    # relay the error!
    raise OSError(e)

改成

else:
    # Our target dir is already a file, or different error,
    # relay the error!
    raise

因为这正是你想要的,让Python重新抛出相同的异常(只是挑剔一下)


还有一件事,也许这个对你有帮助(仅限类Unix系统)。

3

在Python 3.x中,你可以使用 os.makedirs(path, exist_ok=True) 这个命令来创建文件夹。如果你要创建的文件夹已经存在,它不会报错。可是,如果有一个文件的名字和你想创建的文件夹名字一样,那么就会出现 FileExistsError: [Errno 17] 的错误。

你可以用下面的代码来验证一下:

import os

parent = os.path.dirname(__file__)

target = os.path.join(parent, 'target')

os.makedirs(target, exist_ok=True)
os.makedirs(target, exist_ok=True)

os.rmdir(target)

with open(target, 'w'):
    pass

os.makedirs(target, exist_ok=True)

撰写回答