17

问题

是否File.AppendAllText管理来自多个作者的冲突?

研究

我注意到MSDN 文档并没有真正提供任何一种方式,所以我决定反映代码并看看它做了什么。下面是调用的方法File.AppendAllText

private static void InternalAppendAllText(string path, string contents, Encoding encoding)
{
    using (StreamWriter streamWriter = new StreamWriter(path, true, encoding))
    {
        streamWriter.Write(contents);
    }
}

正如你所看到的,它只是利用了一个StreamWriter. 所以,如果我们更深入地研究它,特别是它使用的构造函数,我们会发现它最终调用了这个构造函数:

internal StreamWriter(string path, bool append, Encoding encoding, int bufferSize, bool checkHost) : base(null)
{
    if (path == null)
    {
        throw new ArgumentNullException("path");
    }
    if (encoding == null)
    {
        throw new ArgumentNullException("encoding");
    }
    if (path.Length == 0)
    {
        throw new ArgumentException(Environment.GetResourceString("Argument_EmptyPath"));
    }
    if (bufferSize <= 0)
    {
        throw new ArgumentOutOfRangeException("bufferSize", Environment.GetResourceString("ArgumentOutOfRange_NeedPosNum"));
    }
    Stream streamArg = StreamWriter.CreateFile(path, append, checkHost);
    this.Init(streamArg, encoding, bufferSize, false);
}

具有以下值:

path:        the path to the file
append:      the text to append
encoding:    UTF8NoBOM
bufferSize:  1024
checkHost:   true

并且进一步我们发现,base(null)实现除了设置InternalFormatProvidernull. 所以,如果我们继续挖掘,我们会发现CreateFile

private static Stream CreateFile(string path, bool append, bool checkHost)
{
    FileMode mode = append ? FileMode.Append : FileMode.Create;
    return new FileStream(path, mode, FileAccess.Write, FileShare.Read, 4096, FileOptions.SequentialScan, Path.GetFileName(path), false, false, checkHost);
}

FileStream使用这些参数值创建一个:

path:         the path to the file
mode:         FileMode.Append
access:       FileAccess.Write
share:        FileShare.Read
bufferSize:   4096
options:      FileOptions.SequentialScan
msgPath:      just the file name of the path provided
bFromProxy:   false
useLongPath:  false
checkHost:    true

所以现在我们终于到达了某个地方,因为我们即将利用 Windows API,这就是问题真正开始的地方,因为它FileStream::ctor调用了一个名为Init. 这是一个相当长的方法,但我真的对一行很感兴趣:

this._handle = Win32Native.SafeCreateFile(text3,
    dwDesiredAccess,
    share,
    secAttrs,
    mode,
    num,
    IntPtr.Zero);

当然调用CreateFile,其中参数值是:

text3:            the full path to the file
dwDesiredAccess:  1073741824
share:            1 (FILE_SHARE_READ)
secAttrs:         null
mode:             4 (OPEN_ALWAYS)
num:              134217728 | 1048576 (FILE_FLAG_SEQUENTIAL_SCAN | FILE_FLAG_POSIX_SEMANTICS)

那么,如果我有两个线程试图同时访问同一路径的该调用,Windows 会怎么做?它会打开文件并缓冲写入,以便允许两个消费者写入文件吗?还是我需要利用锁定对象并lock围绕对 的调用AppendAllText

4

3 回答 3

7

只有一个会赢得写入,它将是第一个,任何后续尝试都将失败,直到释放写入锁(即刷新缓冲区并关闭文件) - 但是,它可以同时打开以供读取(取决于权限) .

读取- 允许随后打开文件进行读取。如果未指定此标志,则任何打开文件进行读取的请求(由该进程或其他进程)都将失败,直到文件关闭。但是,即使指定了此标志,仍可能需要其他权限才能访问该文件。

于 2013-04-18T12:58:48.440 回答
5

关键是这个方法:

private static Stream CreateFile(string path, bool append, bool checkHost)
{
    FileMode mode = append ? FileMode.Append : FileMode.Create;
    return new FileStream(path, mode, FileAccess.Write, FileShare.Read, 4096, FileOptions.SequentialScan, Path.GetFileName(path), false, false, checkHost);
}

它以 开头FileShare.Read,意味着其他线程或进程可以打开文件进行读取,但没有其他进程/线程可以打开文件进行写入。

您可能不希望它允许多个并发作者。考虑编写两个非常大的缓冲区。他们很可能最终会被交错。

所以,是的......如果您有多个线程可能附加到该文件,您需要同步访问,可能带有锁。

根据您的应用程序,另一种选择是拥有一个从队列中读取文本并附加到文件的消费者线程。这样,只有一个线程可以访问该文件。其他线程将消息放在写入线程服务的队列中。这很容易用BlockingCollection.

于 2013-04-18T13:09:53.633 回答
2

我知道这个话题很老,但我在阅读https://stackoverflow.com/a/18692934/3789481后发现

通过实现 EventWaitHandle,我可以轻松地防止与 File.AppendAllText 发生冲突

    EventWaitHandle waitHandle = new EventWaitHandle(true, EventResetMode.AutoReset, "SHARED_BY_ALL_PROCESSES");

    Task[] tasks = new Task[ThreadCount];
    for (int counter = 0; counter < ThreadCount; counter++)
    {
        var dividedList = ....
        tasks[counter] = await Task.Factory.StartNew(async () => await RunTask(counter, dividedList, waitHandle));
    }

并将 RunTask 写入文件

    private static async Task RunTask(int threadNum, List<string> ids, EventWaitHandle waitHandle)
    {
        Console.WriteLine($"Start thread {threadNum}");

        foreach (var id in ids)
        {
            // start waiting
            waitHandle.WaitOne();
            File.AppendAllText(@".\Result.txt", text + Environment.NewLine);
            waitHandle.Set();
            // until release

            // ninja code
        }
        Console.WriteLine($"End thread {threadNum}");
    }

我用 500 个线程测试过,效果很好!!

于 2020-02-06T09:02:49.297 回答