使用锁文件作为多个进程之间的锁的正确方法

5

我遇到了一个情况,有两个不同的进程(C++代码由我编写,Java代码由其他人编写)需要读写一些共享数据文件。因此,我尝试编写了一个类来避免竞争条件,类似于这样(注意:此代码已经失效,只是一个示例):

class ReadStatus
{
    bool canRead;
public:
    ReadStatus()
    {
        if (filesystem::exists(noReadFileName))
        {
            canRead = false;
            return;
        }
        ofstream noWriteFile;
        noWriteFile.open (noWriteFileName.c_str());
        if ( ! noWriteFile.is_open())
        {
            canRead = false;
            return;
        }
        boost::this_thread::sleep(boost::posix_time::seconds(1));
        if (filesystem::exists(noReadFileName))
        {
            filesystem::remove(noWriteFileName);
            canRead= false;
            return;
        }
        canRead= true;
    }
    ~ReadStatus()
    {
        if (filesystem::exists(noWriteFileName))
            filesystem::remove(noWriteFileName);
    }
    inline bool OKToRead()
    {
        return canRead;
    }
};

使用方法:

ReadStatus readStatus; //RAII FTW
    if ( ! readStatus.OKToRead())
        return;

这是一个程序的说明,其他程序将有类似的类。思路如下: 1. 检查其他程序是否创建了它的“我是所有者文件”,如果是则退出,否则进入步骤2。 2. 创建我的“我是所有者”文件,再次检查其他程序是否创建了它自己的文件,如果是,则删除我的文件并退出,否则进入步骤3。 3. 进行读取操作,然后删除我的“我是所有者文件”。
请注意,虽然很少发生读写都不出现的情况,但问题在于我仍然看到一种竞态条件的小概率存在,因为理论上其他程序可以检查我的锁定文件的存在性,看到没有文件,然后我创建我的文件,其他程序创建它自己的文件,但在FS创建它的文件之前,我再次检查,发现文件不存在,这样就会发生灾难。这就是为什么我添加了一秒钟的延迟,但作为一个CS迷,我觉得让这样的代码运行令人不安。
当然,我不指望这里有人给我写出解决方案,但如果有人知道一个可靠的代码链接,我会很高兴使用它。 附:数据文件的访问不是读者、写者、读者、写者...它可以是读者、读者、写者、写者、写者、读者、写者... 另一个进程不是用C++编写的,所以boost不在考虑范围内。

1
为什么要重新发明轮子,而不是包装操作系统提供的锁原语? - NPE
@aix - 就像我说的,我不会写整个项目,也不会设计它。当然,如果没有人回答带有“FS互斥”魔法的问题,我会哭泣,然后要求在设计中进行更改,或者希望超时能够足够好地避免竞态条件。 - NoSenseEtAl
3个回答

9
在Unices系统中,传统的纯文件系统锁定方式是使用专用的锁文件,可以通过单个系统调用原子地创建和删除锁文件,其中包括mkdir()rmdir()。您可以通过从不显式测试锁的存在来避免竞争,而是始终尝试获取锁。因此:
lock:
    while mkdir(lockfile) fails
        sleep

unlock:
    rmdir(lockfile)

我认为这甚至可以在NFS上运行(通常对此类事情不太友好)。
但是,您可能还想研究一下适当的文件锁定,这样会更好;我在Linux上使用F_SETLK/F_UNLCK fcntl locks进行此操作(请注意,这些与flock锁定不同,尽管结构名称相同)。这允许您正确地阻止,直到锁定被释放。如果应用程序死机,这些锁也会自动释放,这通常是件好事。此外,这些锁还允许您直接锁定共享文件,而无需单独的锁定文件。这也适用于NFS。
Windows具有非常相似的文件锁定功能,还具有易于使用的全局命名信号量,非常方便进程间同步。

1
哈,不错的主意,甚至都不测试文件是否存在。 - Xeo
厉害的想法,使用mkdir命令。你知道boost::filesystem::create_directory()函数是否也是原子操作吗?如果不是在所有平台都如此,那可能只在Linux上是。 - NoSenseEtAl
1
我完全不知道boost在create_directory()内部做了什么,但我无法想象一个正常的操作系统底层的make-directory系统调用不是原子性的。 - David Given
有没有针对NFS的Windows替代F_SETLK/F_UNLCK fcntl锁定并在程序终止时销毁锁定的功能? - Sir Digby Chicken Caesar
我上次在Windows上使用NFS是很多很多年前的事了。毫无头绪。 - David Given

4
据我所见,您不能可靠地将文件用作多个进程的锁。问题在于,当您在一个线程中创建文件时,可能会发生中断,并且操作系统会切换到另一个进程,因为I/O需要很长时间。删除锁定文件也是如此。
如果可以,请查看Boost.Interprocess,了解同步机制部分。

抱歉,但是另一个进程不是用C++或JAVA编写的。我会更新原始问题。 - NoSenseEtAl
1
这是不正确的(至少对于Windows来说)。你可以可靠地使用文件作为锁定,只要你知道你在做什么。请阅读 Opportunistic Locks 的相关资料。http://msdn.microsoft.com/en-us/library/windows/desktop/aa365007(v=vs.85).aspx - Phillip Scott Givens

1

通常我不建议在构造函数/析构函数中进行可能会抛出异常的API调用(请参阅boost::filesystem::remove文档),或者在没有catch块的情况下进行抛出调用,但这并不是你所问的问题。

如果你使用的是Windows系统,可以尝试使用Overlapped IO库。否则,你考虑过使用进程间共享内存吗?

编辑:刚看到另一个进程是Java。你仍然可以创建一个命名互斥锁,该锁可以在进程之间共享,并用它来在文件IO位周围创建锁,以便它们必须轮流写入。很抱歉我不懂Java,不知道这是否比共享内存更可行。


感谢提供删除信息,但对于另一个boost:我的错是没有说明我不能使用boost,因为它是用于与JAVA进程进行进程间通信。我现在已经更新了原始问题。此外,它是针对Linux的,但当然我更喜欢跨平台的解决方案。 - NoSenseEtAl
关于缺少catch块的问题,你认为有必要加上吗?如果移除失败并且它是一个锁定机制,我不认为有任何合理的方法可以从中恢复。我应该像疯子一样while循环直到没有异常,还是有更好的处理方式? - NoSenseEtAl
1
理想情况下,如果您想要一个删除请求队列或其他更高级的功能,并在删除失败时推送另一个尝试,则至少应记录错误或通知用户,但通常有一个可行的原因导致删除失败(例如某些其他进程正在写入它)。 - AJG85
1
也请阅读此文以了解何时何不应该抛出异常,以及如何处理异常(构造函数和析构函数都有讲述):http://www.parashift.com/c++-faq-lite/exceptions.html - AJG85
1
我喜欢信息的随意呈现。Herb Sutter有一篇更好的文章,但我找不到链接。要点是不要在析构函数中抛出异常,但如果需要发出创建失败的信号,则可以在构造函数中抛出异常,并确保处理它。 - AJG85
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接