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

时间:2013-03-26 03:31:59

标签: c# .net winapi file-locking

我正在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共享中,因此其他用户和进程可以访问它。 问题不是在我自己的进程中管理锁。我试图解决的问题是如何独占锁定文件然后移动/删除它而不释放锁

6 个答案:

答案 0 :(得分:6)

我想到了两种解决方案。

第一个也是最简单的方法是让线程将文件重命名为其他线程不会触及的内容。像“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);

这样一来,如果有人在你开始删除它之前就已经达到了它,那么就没有什么可以修改的了。

答案 1 :(得分:4)

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

答案 2 :(得分:3)

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

答案 3 :(得分:3)

如果您在使用这些文件的多台服务器上有多个进程,那么我所知道的最强大的方法甚至可以正常工作。

不是自己锁定文件,而是创建一个用于锁定的临时文件,这样你可以毫无问题地解锁/移动/删除原始文件,但仍然要确保至少任何服务器/线程上运行的代码副本/ process不会同时尝试使用该文件。

Psuedo代码:

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和大多数旧数据库系统。阅读:经过充分测试。

答案 4 :(得分:0)

此解决方案认为不会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();

答案 5 :(得分:-1)

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