【问题标题】:How to prevent a race condition when multiple processes attempt to write to and then read from a file at the same time当多个进程尝试同时写入然后从文件中读取时,如何防止竞争条件
【发布时间】:2015-08-05 02:31:43
【问题描述】:

我有以下代码(为清楚起见进行了简化):

import os
import errno
import imp


lib_dir = os.path.expanduser('~/.brian/cython_extensions')
module_name = '_cython_magic_5'
module_path = os.path.join(lib_dir, module_name + '.so')
code = 'some code'

have_module = os.path.isfile(module_path)
if not have_module:
    pyx_file = os.path.join(lib_dir, module_name + '.pyx')

    # THIS IS WHERE EACH PROCESS TRIES TO WRITE TO THE FILE.  THE CODE HERE 
    # PREVENTS A RACE CONDITION.
    try:
        fd = os.open(pyx_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
    except OSError as e:
        if e.errno == errno.EEXIST:
            pass
        else:
            raise
    else:
        os.fdopen(fd, 'w').write(code)

# THIS IS WHERE EACH PROCESS TRIES TO READ FROM THE FILE.  CURRENTLY THERE IS A
# RACE CONDITION.
module = imp.load_dynamic(module_name, module_path)

(上面部分代码是从this answer借来的。)

当同时运行多个进程时,此代码只会打开一个进程并写入pyx_file(假设pyx_file 尚不存在)。问题是,当这个进程正在写入pyx_file 时,其他进程会尝试加载pyx_file——在后面的进程中会出现错误,因为在它们读取pyx_file 时,它是不完整的。 (具体来说,ImportErrors 被引发,因为进程正在尝试导入文件的内容。)

避免这些错误的最佳方法是什么?一个想法是让进程在一个while循环中不断尝试导入pyx_file,直到导入成功。 (这个解决方案似乎不是最理想的。)

【问题讨论】:

    标签: python io race-condition python-import python-os


    【解决方案1】:

    每次访问文件时,使用 PID 一个空文件来锁定。

    示例用法:

    from mercurial import error, lock
    
    try:
        l = lock.lock("/tmp/{0}.lock".format(FILENAME), timeout=600) # wait at most 10 minutes
        # do something
    except error.LockHeld:
         # couldn't take the lock
    else:
        l.release()
    

    来源:Python: module for creating PID-based lockfile?

    这会给你一个大致的想法。此方法用于 OO、vim 和其他应用程序。

    【讨论】:

    • 好的,我会尝试修改它以使用我的代码。由于我的代码已发布在我的问题中,因此欢迎您也这样做并相应地更新您的答案。
    • 这对我来说也不理想,因为我的代码来自一个没有 mercurial 作为依赖项的包。
    • 你可以通过使用Python IO创建一个pid文件来锁定它。
    • @dbliss,所有与 SVR4 兼容的类 UNIX 系统都具有flock()fcntl(LOCK_*) 或两者兼有,如果没有flock() 调用,Python 的fcntl.flock() 调用将自动回退到fcntl()可用。与 SVR4 不兼容的 UNIX 近来闻所未闻——要说清楚,那是 80 年代后期。
    • @dbliss, ...唯一会遇到可移植性问题的平台是 Windows,对此,请参阅 stackoverflow.com/questions/1422368/fcntl-substitute-on-windows
    【解决方案2】:

    这样做的方法是每次打开它时都获取一个独占锁。写入器在写入数据时持有锁,而读取器阻塞,直到写入器通过 fdclose 调用释放锁。如果文件已经被部分写入并且写入过程异常退出,这当然会失败,因此如果无法加载模块,应该显示删除文件的适当错误:

    import os
    import fcntl as F
    
    def load_module():
        pyx_file = os.path.join(lib_dir, module_name + '.pyx')
    
        try:
            # Try and create/open the file only if it doesn't exist.
            fd = os.open(pyx_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY):
    
            # Lock the file exclusively to notify other processes we're writing still.
            F.flock(fd, F.LOCK_EX)
            with os.fdopen(fd, 'w') as f:
                f.write(code)
    
        except OSError as e:
            # If the error wasn't EEXIST we should raise it.
            if e.errno != errno.EEXIST:
                raise
    
        # The file existed, so let's open it for reading and then try and
        # lock it. This will block on the LOCK_EX above if it's held by
        # the writing process.
        with file(pyx_file, "r") as f:
            F.flock(f, F.LOCK_EX)
    
        return imp.load_dynamic(module_name, module_path)
    
    module = load_module()
    

    【讨论】:

    • 太棒了。看起来像我需要的。一个问题:with 块是否必要?在我的代码中,“读取”发生在对 imp.load_dynamic 的调用中。
    • 是的。这是在尝试加载模块之前等待编写器完成写入的块。没有它,它可能会在作者完成写入之前尝试加载模块。锁是写入器和读取器用来传达模块已就绪的 IPC。
    • 没关系,我已经对其进行了一些修改,以便文件现在会自动关闭。我认为 fdopen 有干扰,但无论如何这个新编辑更安全。
    • 好的,我正在尝试,但你确定它有效吗?因为fd 是在with 块之外定义的,所以即使fwith 块关闭,它也不会保持打开状态吗?
    • 通常情况下会这样,但它会隐式关闭,因为我们正在关闭我们从它创建的file,这也关闭了底层fd
    猜你喜欢
    • 2018-05-22
    • 2020-02-11
    • 2020-12-20
    • 2012-10-14
    • 1970-01-01
    • 2019-12-09
    • 2019-02-15
    • 2017-05-19
    • 1970-01-01
    相关资源
    最近更新 更多