为什么File.Move允许2个线程同时移动同一个文件?

时间:2014-07-28 09:13:22

标签: c# multithreading file-io

我们目前有一个应用程序监视新文件的文件夹。为了使其具有容错能力并能够一次处理更多文件,我们希望能够在不同的计算机上运行此应用程序的多个实例。我们使用File.Move来锁定"一个文件,并确保一次只有一个线程可以处理文件。

为了测试只有一个应用程序和/或线程可以对文件执行File.Move,我创建了一个简单的应用程序(基于原始应用程序的代码),每个应用程序创建了10个线程,监视一个文件夹,当每个线程检测到一个新文件时,它会对其执行File.Move并更改文件的扩展名,以尝试阻止其他线程执行此操作。

我在运行此应用程序的多个副本时遇到了一个问题(并且它自己运行),其中2个线程(在同一个应用程序中或不同的应用程序中)都成功执行File.Move而没有抛出任何异常,但是最后执行它的线程(我更改了文件的扩展名以包含DateTime.Now.ToFileTime()),成功重命名了该文件。 我查看了File.Move做了什么,并在执行操作之前检查文件是否存在,然后调用Win32Native.MoveFile来执行移动。

正如我所料,所有其他线程/应用程序都会抛出异常。

这是一个问题的原因是:

  1. 我认为一次只有一个帖子可以对文件执行File.Move
  2. 我需要可靠地让一个应用程序/线程一次只能处理一个文件。
  3. 以下是执行File.Move的代码:

    public bool TryLock(string originalFile, out string changedFileName)
    {
        FileInfo fileInfo = new FileInfo(originalFile);
        changedFileName = Path.ChangeExtension(originalFile, ".original." + DateTime.Now.ToFileTime());
        try
        {
            File.Move(originalFile, changedFileName);
        }
        catch (IOException ex)
        {
            Console.WriteLine("{3} - Thread {1}-{2} File {0} is already in use", fileInfo.Name, Thread.CurrentThread.ManagedThreadId, id, DateTime.Now.ToLongTimeString());
            return false;
        }
        catch (Exception ex)
        {
            Console.WriteLine("{3} - Thread {1}-{2} File {0} error {4}", fileInfo.Name, Thread.CurrentThread.ManagedThreadId, id, DateTime.Now.ToLongTimeString(), ex);
            return false;
        }
        return true;
    }
    

    注意 - id只是我分配给每个线程进行记录的序号。

    我在带有NTFS的SSD上运行Windows 7 Enterprise SP1。

4 个答案:

答案 0 :(得分:1)

MSDN description我假设File.Move没有以独占模式打开文件。

  

如果您尝试跨磁盘卷移动文件并且该文件正在使用中,   该文件被复制到目标,但不会从中删除   源。

无论如何,我认为你最好创建自己的移动机制并让它在复制之前以独占模式打开文件(然后删除它):

File.Open(pathToYourFile, FileMode.Open, FileAccess.Read, FileShare.None);

如果移动操作已在进行中,则其他线程将无法打开它。在复制完成之前(因此您需要处理文件句柄)并删除它时,您可能会遇到竞争条件问题。

答案 1 :(得分:1)

使用File.Move作为锁定不起作用。正如@ marceln的回答所述,它不会删除它已在其他地方使用过的源文件,并且没有“锁定”行为,你无法继续它。

我建议使用BlockingCollection<T>管理文件处理:

// Assuming this BlockingCollection is already filled with all string file paths
private BlockingCollection<string> _blockingFileCollection = new BlockingCollection<string>();

public bool TryProcessFile(string originalFile, out string changedFileName)
{
    FileInfo fileInfo = new FileInfo(originalFile);
    changedFileName = Path.ChangeExtension(originalFile, ".original." + DateTime.Now.ToFileTime());

    string itemToProcess;
    if (_blockingFileCollection.TryTake(out itemToProcess))
    {
        return false;
    }

    // The file should exclusively be moved by one thread,
    // all other should return false.

    File.Move(originalFile, changedFileName);
    return true;
}

答案 2 :(得分:1)

  

您是在跨卷还是在卷内移动?在后一种情况下,不需要复制。

  

@usr在制作中,一旦线程被锁定&#34;一个文件,我们将通过网络共享移动它

我不确定这是真正的动作还是复制操作。无论如何,你可以:

  1. 专门打开文件
  2. 复制数据
  3. 按句柄(Deleting or Renaming a file using an open handle
  4. 删除来源

    这允许您在移动期间锁定该文件中的其他进程。它更像是一种解决方法,而不是一种真正的解决方案。希望它有所帮助。

    请注意,在移动期间,文件不可用,其他进程将收到访问它的错误。您可能需要在操作之间存在时间延迟的重试循环。

    这是另一种选择:

    1. 将文件复制到目标文件夹,其中包含读者忽略的不同扩展名
    2. 以原子方式重命名文件以删除扩展名
    3. 在同一卷上重命名始终是原子的。读者可能会在很短的时间内收到共享冲突错误。同样,您需要重试循环或容忍一个非常小的不可用窗口。

答案 3 :(得分:1)

基于@marceln和@YuvalItzchakov回答/评论,我尝试了以下内容,这似乎给出了更可靠的结果:

using (var readFileStream = File.Open(originalFile, FileMode.Open, FileAccess.Read, FileShare.Delete))
{
    readFileStream.Lock(0, readFileStream.Length - 1);
    File.Move(originalFile, changedFileName);
    readFileStream.Unlock(0, readFileStream.Length - 1);
}

我想使用Windows自己的文件复制,因为它应该比复制流更有效,在生产中我们将文件从一个网络共享移动到另一个网络共享。