同一文件的并发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" 是它有意尝试对单个文件执行并发操作。虽然文件系统本身将确保它保持一致,但要由您自己的代码来确保此类操作得到协调,以便结果可预测且可靠。
这里明确说明了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" 是它有意尝试对单个文件执行并发操作。虽然文件系统本身将确保它保持一致,但要由您自己的代码来确保此类操作得到协调,以便结果可预测且可靠。