独占锁定文件,然后删除/移动它

Posted

技术标签:

【中文标题】独占锁定文件,然后删除/移动它【英文标题】:Lock file exclusively then delete/move it 【发布时间】:2013-03-15 18:23:28 【问题描述】:

我在 C# 中实现了一个类,它应该监视一个目录,在文件被删除时对其进行处理,然后在处理完成后立即删除(或移动)已处理的文件。由于可以有多个线程运行此代码,第一个获取文件的线程将其独占锁定,因此没有其他线程将读取同一文件并且没有外部进程或用户可以以任何方式访问 .我想保持锁定直到文件被删除/移动,所以没有另一个线程/进程/用户访问它的风险。

到目前为止,我尝试了 2 个实现选项,但都没有按我的意愿工作。

选项 1

FileStream fs = file.Open(FileMode.Open, FileAccess.Read, FileShare.Delete);
//Read and process
File.Delete(file.FullName); //Or File.Move, based on a flag
fs.Close();

选项 2

FileStream fs = file.Open(FileMode.Open, FileAccess.Read, FileShare.None);
//Read and process
fs.Close();
File.Delete(file.FullName); //Or File.Move, based on a flag

选项 1 的问题是其他进程可以访问该文件(他们可以删除、移动、重命名),而该文件应该被完全锁定。

选项2的问题是文件在被删除之前是解锁的,所以其他进程/线程可以在删除之前锁定文件,所以删除会失败。

我正在寻找一些可以使用我已经拥有独占访问权限的文件句柄执行删除的 API。

编辑

被监控的目录位于 pub 共享中,因此其他用户和进程可以访问它。 问题不是在我自己的进程中管理锁。我要解决的问题是如何以独占方式锁定文件,然后在不释放锁定的情况下移动/删除它

【问题讨论】:

能否在处理之前将其重命名为其他线程无法获取的名称? 文件名可以是任何东西,因此重命名将无济于事。即使文件名很重要,我也想防止任何其他有权访问该文件夹的进程或用户在它被锁定时触摸它。 【参考方案1】:

想到了两个解决方案。

第一个也是最简单的方法是让线程将文件重命名为其他线程不会触及的名称。像“filename.dat.<unique number>”这样的东西,其中<unique number> 是特定于线程的东西。然后线程可以随心所欲地处理文件。

如果两个线程同时获取文件,则只有其中一个线程能够重命名它。您必须处理其他线程中发生的 IOException,但这应该不是问题。

另一种方法是让一个线程监视目录并将文件名放入BlockingCollection。工作线程从该队列中获取项目并处理它们。因为只有一个线程可以从队列中获取该特定项目,所以没有争用。

BlockingCollection 解决方案的设置稍微复杂一点(但只是稍微复杂一点),但应该比具有多个线程监视同一目录的解决方案执行得更好。

编辑

您编辑的问题使问题发生了很大变化。如果您的文件位于可公开访问的目录中,则在将其放置在该目录与您的线程锁定它之间的任何时间点,它都有被查看、修改或删除的风险。

由于在打开文件时无法移动或删除文件(我不知道),因此最好的办法是让线程将文件移动到不可公开访问的目录。理想情况下到一个被锁定的目录,这样只有运行您的应用程序的用户才能访问。所以你的代码变成了:

File.Move(sourceFilename, destFilename);
// the file is now in a presumably safe place.
// Assuming that all of your threads obey the rules,
// you have exclusive access by agreement.

编辑#2

另一种可能性是独占打开文件并使用您自己的复制循环复制它,复制完成后让文件保持打开状态。然后您可以倒带文件并进行处理。比如:

var srcFile = File.Open(/* be sure to specify exclusive access */);
var destFile = File.OpenWrite(/* destination path */);
// copy the file
var buffer = new byte[32768];
int bytesRead = 0;
while ((bytesRead = srcFile.Read(buffer, 0, buffer.Length)) != 0)

    destFile.Write(buffer, 0, bytesRead);

// close destination
destFile.Close();
// rewind source
srcFile.Seek(0, SeekOrigin.Start);
// now read from source to do your processing.
// for example, to get a StreamReader, just pass the srcFile stream to the constructor.

有时您可以处理然后复制。这取决于您完成处理后流是否保持打开状态。通常,代码会执行以下操作:

using (var strm = new StreamReader(srcStream, ...))

    // do stuff here

这最终关闭了流和srcStream。您必须像这样编写代码:

using (var srcStream = new FileStream( /* exclusive access */))

    var reader = new StreamReader(srcStream, ...);
    // process the stream, leaving the reader open
    // rewind srcStream
    // copy srcStream to destination
    // close reader

可行,但笨拙。

哦,如果您想在删除文件之前消除有人读取文件的可能性,只需在关闭文件之前将文件截断为 0。如:

srcStream.Seek(0, SeekOrigin.Begin);
srcStream.SetLength(0);

这样,如果有人在您删除它之前确实找到了它,那么就没有什么可修改的了,等等。

【讨论】:

这些解决方案都不会阻止外部用户/进程重命名或删除锁定的文件(用FileShare.Delete 锁定)。使用BlockingCollection 或其他一些同步机制在我自己的进程中管理锁将是微不足道的。 让用户在文件被拾取之前查看/修改/删除文件很好(文件实际上是手动删除的,所以如果有人错误地删除了文件并且幸运地在它被锁定之前删除了,那就是好的)。将文件移动到安全位置的建议解决方案会起作用,即使它会带来性能损失(它会从网络共享复制到本地目录,然后再回到同一个网络共享到存档文件夹中)。谢谢@jim-mischel,如果我真的找不到移动文件“重新使用”句柄的方法,我会将其标记为已接受的答案。 @GDemartini:只是好奇为什么“安全”目录不能在网络共享上。你不能在那里设置权限来限制访问吗? 我不是共享的所有者,因此我不应该创建文件夹或更改其结构,即使我在文件系统上拥有这样做的权限。 根据documentation for File.Move >如果您尝试跨磁盘卷移动文件并且该文件正在使用中,则该文件将复制到目标位置,但不会从源中删除。 【参考方案2】:

这是我所知道的最可靠的方法,如果您在多台服务器上有多个进程处理这些文件,它甚至可以正常工作。

与其锁定文件本身,不如创建一个用于锁定的临时文件,这样您就可以毫无问题地解锁/移动/删除原始文件,但仍要确保至少在任何服务器/线程上运行您的代码的任何副本/process 不会尝试同时处理该文件。

伪代码:

try

    // get an exclusive cross-server/process/thread lock by opening/creating a temp file with no sharing allowed
    var lockFilePath = $"file.lck";
    var lockFile = File.Open(lockFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);

    try
    
        // open file itself with no sharing allowed, in case some process that does not use our locking schema is trying to use it
        var fileHandle = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.None);

        // TODO: add processing -- we have exclusive access to the file, and also the locking file

        fileHandle.Close();

        // at this point it is possible for some other process that does not use our locking schema to lock the file before we
        //  move it, causing us to process this file again -- we would always have to handle issues where we failed to move
        //  the file anyway (maybe we just lost power, or crashed?) so we had to design around this no matter what

        File.Move(file, archiveDestination);
    
    finally
    
        lockFile.Close();

        try
        
            File.Delete(lockFilePath);
        
        catch (Exception ex)
        
            // another process opened locked file after we closed it, before it was deleted -- safely ignore, other process will delete lock file
        
    

catch (Exception ex)

    // another process already has exclusive access to the lock file, we don't need to do anything
    // or we failed while processing, in which case we did not move the file so it will be tried again by this process or another

这种模式的一个好处是它也可以用于文件存储支持锁定的时候。例如,如果您尝试在 FTP/SFTP 服务器上处理文件,您可以让您的临时锁定文件使用普通驱动器(或 SMB 共享)——因为锁定文件不必与文件本身。

我不能相信这个想法,它比 PC 存在的时间更长,并且被 Microsoft Word、Excel、Access 和大多数旧数据库系统等大量应用程序使用。阅读:经过良好测试。

【讨论】:

【参考方案3】:

文件系统本身本质上是易变的,因此很难尝试做你想做的事。这是文件系统中的经典竞争条件。使用选项 2,您可以选择将文件移动到您在工作之前创建的“处理”或暂存目录。。 YMMV 关于性能,但您至少可以对其进行基准测试,看看它是否符合您的需求。

【讨论】:

【参考方案4】:

您可能需要从生成线程实现某种形式的共享/同步列表。如果父线程通过定期检查目录来跟踪文件,那么它可以将它们交给子线程,这样就可以消除锁定问题。

【讨论】:

如果我的进程是唯一可以访问文件的进程,但其他用户/进程可以访问它们,它会起作用。【参考方案5】:

这个解决方案,虽然不是 100% 防水,但很可能会满足您的需求。 (它为我们做了。)

使用两个锁,它们一起为您提供对文件的独占访问权限。当您准备好删除该文件时,您释放其中一个,然后删除该文件。剩余的锁仍然会阻止大多数其他进程获得锁。

FileInfo file = ...

// Get read access to the file and only allow other processes write or delete access.
// Keeps others from locking the file for reading.
var readStream = file.Open(FileMode.Open, FileAccess.Read, FileShare.Write | FileShare.Delete);
FileStream preventWriteAndDelete;
try

    // Now try to get a lock on than only allows others to read the file.  We can acquire both
    // locks because they each allow the other.  Together, they give us exclusive access to the
    // file.
    preventWriteAndDelete = file.Open(FileMode.Open, FileAccess.Write, FileShare.Read);

catch

    // We couldn't get the second lock, so release the first.
    readStream.Dispose();
    throw;

现在您可以读取文件(使用readStream)。如果您需要写入它,则必须使用另一个流来执行此操作。

当你准备好删除文件时,你首先释放阻止写入和删除的锁,同时仍然持有阻止读取的锁。

preventWriteAndDelete.Dispose(); // Release lock that prevents deletion.
file.Delete();
// This lock specifically allowed deletion, but with the file gone, we're done with it now.
readStream.Dispose(); 

另一个进程(或线程)获得文件锁定的唯一机会是,如果它请求共享写锁定,则该锁定赋予它只写访问权限并允许其他人写入文件。这不是很常见。大多数进程尝试共享读锁(允许其他人读取但不能写入或删除的读访问)或排他写锁(不共享的写或读/写访问)。这两种常见情况都会失败。共享读/写锁(请求读/写访问并允许其他人相同)也将失败。

此外,进程请求和获取共享写锁的机会窗口非常小。如果一个进程正在努力获取这样的锁,那么它可能会成功,但很少有应用程序这样做。因此,除非您的场景中有这样的应用程序,否则此策略应该可以满足您的需求。

您也可以使用相同的策略来移动文件。

preventWriteAndDelete.Dispose();
file.MoveTo(destination);
readStream.Dispose();

【讨论】:

【参考方案6】:

您可以使用MoveFileEx API function 将文件标记为在下次重新启动时删除。 Source

【讨论】:

以上是关于独占锁定文件,然后删除/移动它的主要内容,如果未能解决你的问题,请参考以下文章

当锁定文件的另一个 FileChannnel 关闭时,文件的独占锁定被降级

独占打开文件/锁定文件

为啥原子操作需要独占缓存访问?

Cloud Firestore 文档锁定

I/O:FileLock

如果文件存在,如何锁定文件,不要创建它?