独占锁定文件,然后删除/移动它
本文关键字:删除 移动 然后 锁定 文件 | 更新日期: 2023-09-27 18:05:45
我正在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共享中,因此其他用户和进程可以访问它。问题不是在我自己的进程中管理锁。我试图解决的问题是如何锁定文件排他然后移动/删除它而不释放锁
两个解决方案浮现在脑海中。
第一个也是最简单的方法是让线程将文件重命名为其他线程不会触及的名称。比如"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);
这样,如果有人在你删除它之前得到它,没有什么可修改的,等等
这是我所知道的最健壮的方法,即使你在多个服务器上有多个进程处理这些文件,它也会正确工作。
不要锁定文件本身,而是创建一个临时文件来锁定,这样你就可以解锁/移动/删除原始文件而不会出现问题,但仍然要确保至少在任何服务器/线程/进程上运行的任何代码副本不会同时尝试使用该文件。
推出伪代码:
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共享)——因为锁定文件不必与文件本身位于相同的位置。
我不能把这个想法归功于自己,它的出现时间比个人电脑还要长,很多应用程序都在使用它,比如微软的Word、Excel、Access和大多数旧的数据库系统。阅读:良好测试
文件系统本身在本质上是不稳定的,所以很难尝试做您想做的事情。这是文件系统中的典型竞争条件。对于选项2,您可以选择将文件移动到您在执行工作之前创建的的"处理"或暂存目录。在性能上,你至少可以对它进行基准测试,看看它是否符合你的需求。
你可能需要从生成线程实现某种形式的共享/同步列表。如果父线程通过定期检查目录来跟踪文件,那么它可以将它们交给子线程,这将消除锁定问题。
这个解决方案虽然不是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();
您可以使用 MoveFileEx
API函数来标记文件以便在下次重新启动时删除。源