16

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

4

6 回答 6

7

想到了两个解决方案。

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

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

于 2013-03-26T04:44:13.087 回答
6

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

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

伪代码:

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 和大多数旧数据库系统等大量应用程序使用。阅读:经过良好测试。

于 2017-04-22T21:10:56.477 回答
4

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

于 2013-03-26T03:43:16.420 回答
3

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

于 2013-03-26T03:40:17.417 回答
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();
于 2019-06-07T23:31:32.070 回答
-1

您可以使用MoveFileExAPI 函数在下次重新启动时将文件标记为删除。资源

于 2013-03-26T03:45:00.393 回答