同一文件的并发File.Move

Concurrent File.Move of the same file

这里明确说明了File.Move是原子操作:Atomicity of File.Move.

但以下代码片段导致 多次移动同一文件的可见性

有人知道这段代码有什么问题吗?

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace FileMoveTest
{
    class Program
    {
        static void Main(string[] args)
        {
            string path = "test/" + Guid.NewGuid().ToString();

            CreateFile(path, new string('a', 10 * 1024 * 1024));

            var tasks = new List<Task>();

            for (int i = 0; i < 10; i++)
            {
                var task = Task.Factory.StartNew(() =>
                {
                    try
                    {
                        string newPath = path + "." + Guid.NewGuid();

                        File.Move(path, newPath);

                        // this line does NOT solve the issue
                        if (File.Exists(newPath))
                            Console.WriteLine(string.Format("Moved {0} -> {1}", path, newPath));
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(string.Format("  {0}: {1}", e.GetType(), e.Message));
                    }
                });

                tasks.Add(task);
            }

            Task.WaitAll(tasks.ToArray());
        }

        static void CreateFile(string path, string content)
        {
            string dir = Path.GetDirectoryName(path);

            if (!Directory.Exists(dir))
            {
                Directory.CreateDirectory(dir);
            }

            using (FileStream f = new FileStream(path, FileMode.OpenOrCreate))
            {
                using (StreamWriter w = new StreamWriter(f))
                {
                    w.Write(content);
                }
            }
        }
    }
}

矛盾的输出如下。似乎该文件已多次移动到不同位置。在磁盘上只有其中一个存在。有什么想法吗?

Moved test/eb85560d-8c13-41c1-926a-6871be030742 -> test/eb85560d-8c13-41c1-926a-6871be030742.0018d317-ed7c-4732-92ac-3bb974d29017
Moved test/eb85560d-8c13-41c1-926a-6871be030742 -> test/eb85560d-8c13-41c1-926a-6871be030742.3965dc15-7ef9-4f36-bdb7-94a5939b17db
Moved test/eb85560d-8c13-41c1-926a-6871be030742 -> test/eb85560d-8c13-41c1-926a-6871be030742.fb66306a-5a13-4f26-ade2-acff3fb896be
Moved test/eb85560d-8c13-41c1-926a-6871be030742 -> test/eb85560d-8c13-41c1-926a-6871be030742.c6de8827-aa46-48c1-b036-ad4bf79eb8a9
System.IO.FileNotFoundException: Could not find file 'C:\file-move-test\test\eb85560d-8c13-41c1-926a-6871be030742'.
System.IO.FileNotFoundException: Could not find file 'C:\file-move-test\test\eb85560d-8c13-41c1-926a-6871be030742'.
System.IO.FileNotFoundException: Could not find file 'C:\file-move-test\test\eb85560d-8c13-41c1-926a-6871be030742'.
System.IO.FileNotFoundException: Could not find file 'C:\file-move-test\test\eb85560d-8c13-41c1-926a-6871be030742'.
System.IO.FileNotFoundException: Could not find file 'C:\file-move-test\test\eb85560d-8c13-41c1-926a-6871be030742'.
System.IO.FileNotFoundException: Could not find file 'C:\file-move-test\test\eb85560d-8c13-41c1-926a-6871be030742'.

生成的文件在这里:

eb85560d-8c13-41c1-926a-6871be030742.fb66306a-5a13-4f26-ade2-acff3fb896be

UPDATE. 我可以确认检查 File.Exists 也 NOT 解决问题 - 它可以报告单个文件真的搬进了几个不同的地方。

解决方案。我最终得到的解决方案如下:在对源文件进行操作之前创建特殊的 "lock" 文件,如果它成功了,那么我们可以确定只有这个线程可以独占访问该文件,我们可以安全地做任何我们想做的事情想。下面是创建 suck "lock" 文件的正确参数集。

File.Open(lockPath, FileMode.CreateNew, FileAccess.Write);

Does anyone know what is wrong with this code?

我想这取决于你所说的 "wrong"。

恕我直言,您看到的行为并非意外,至少如果您使用的是 NTFS(其他文件系统的行为可能类似也可能不同)。

底层 OS API(MoveFile() and MoveFileEx() 函数)的文档并不具体,但通常 API 是线程安全的,因为它们保证文件系统不会被并发操作破坏(当然,你自己的数据可能会被破坏,但它会以文件系统一致的方式完成)。

最有可能发生的是,随着移动文件操作的进行,它首先从给定目录 link 获取实际文件句柄(在 NTFS 中,所有 "file names" 你看到的实际上很难 links 到底层文件对象)。获得该文件句柄后,API 然后为底层文件对象创建一个新文件名(即作为硬 link),然后删除以前的硬 link.

当然,随着这个过程的进行,在线程获得底层文件句柄和删除原始硬 link 之间的时间内存在 window。这允许一些但不是所有其他并发移动操作看起来成功。 IE。最终原来的硬盘 link 不存在并且进一步尝试移动它不会成功。

毫无疑问,以上是过于简单化了。文件系统行为可能很复杂。特别是,您所说的观察结果是,当一切都说完之后,您只会得到文件的一个实例。这表明 API 也确实以某种方式协调了各种操作,因此只有一个新创建的硬 link 幸存下来,可能是由于 API 实际上只是重命名了关联的hard link 检索文件对象句柄后,而不是创建一个新句柄并删除旧句柄(实现细节)。


归根结底,代码的 "wrong" 是它有意尝试对单个文件执行并发操作。虽然文件系统本身将确保它保持一致,但要由您自己的代码来确保此类操作得到协调,以便结果可预测且可靠。