【问题标题】:Writing to file in a thread safe manner以线程安全的方式写入文件
【发布时间】:2014-05-07 15:38:48
【问题描述】:

Stringbuilder 异步写入文件。此代码控制一个文件,向其中写入一个流并释放它。它处理来自异步操作的请求,这些请求可能随时进来。

FilePath 是针对每个类实例设置的(因此锁定 Object 是针对每个实例的),但由于这些类可能共享 FilePaths,因此存在潜在的冲突。这种冲突,以及来自类实例之外的所有其他类型,都将通过重试来处理。

此代码是否适合其用途?有没有更好的方法来处理这个问题,这意味着更少(或不)依赖捕获和重试机制?

还有我如何避免捕获因其他原因而发生的异常。

public string Filepath { get; set; }
private Object locker = new Object();

public async Task WriteToFile(StringBuilder text)
    {
        int timeOut = 100;
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        while (true)
        {
            try
            {
                //Wait for resource to be free
                lock (locker)
                {
                    using (FileStream file = new FileStream(Filepath, FileMode.Append, FileAccess.Write, FileShare.Read))
                    using (StreamWriter writer = new StreamWriter(file, Encoding.Unicode))
                    {
                        writer.Write(text.ToString());
                    }
                }
                break;
            }
            catch
            {
                //File not available, conflict with other class instances or application
            }
            if (stopwatch.ElapsedMilliseconds > timeOut)
            {
                //Give up.
                break;
            }
            //Wait and Retry
            await Task.Delay(5);
        }
        stopwatch.Stop();
    }

【问题讨论】:

  • 您的实际文件写入不是异步的。你的意思是让他们成为?
  • @StephenCleary,是的,最初有,但你不应该在锁中等待,所以我不得不改变它。我不确定这会在多大程度上破坏整个事情。
  • 这听起来应该在 codereview tbh 上
  • 也许吧。无论如何感谢您的帮助。
  • 是否有其他进程可以访问文件路径?因为对于单个应用并发,您可以删除大部分代码。

标签: c# multithreading thread-safety filesystems


【解决方案1】:

您如何处理这在很大程度上取决于您的写作频率。如果您不经常编写相对少量的文本,那么只需使用静态锁即可。无论如何,这可能是您最好的选择,因为磁盘驱动器一次只能满足一个请求。假设你所有的输出文件都在同一个驱动器上(也许不是一个公平的假设,但请容忍我),在应用程序级别的锁定和在操作系统级别完成的锁定之间不会有太大的区别。

因此,如果您将locker 声明为:

static object locker = new object();

您可以放心,您的程序中不会与其他线程发生冲突。

如果您希望这个东西是防弹的(或至少合理地如此),您就无法避免捕获异常。坏事可能发生。您必须以某种方式处理异常。你在面对错误时所做的完全是另一回事。如果文件被锁定,您可能需要重试几次。如果您收到错误的路径或文件名错误或磁盘已满或任何其他错误,您可能想要终止该程序。再次,这取决于你。但是你无法避免异常处理,除非你可以接受程序因错误而崩溃。

顺便说一句,您可以替换所有这些代码:

                using (FileStream file = new FileStream(Filepath, FileMode.Append, FileAccess.Write, FileShare.Read))
                using (StreamWriter writer = new StreamWriter(file, Encoding.Unicode))
                {
                    writer.Write(text.ToString());
                }

只需一次调用:

File.AppendAllText(Filepath, text.ToString());

假设您使用的是 .NET 4.0 或更高版本。见File.AppendAllText

您可以处理此问题的另一种方法是让线程将其消息写入队列,并有一个专用线程为该队列提供服务。你会有一个BlockingCollection 的消息和相关的文件路径。例如:

class LogMessage
{
    public string Filepath { get; set; }
    public string Text { get; set; }
}

BlockingCollection<LogMessage> _logMessages = new BlockingCollection<LogMessage>();

您的线程将数据写入该队列:

_logMessages.Add(new LogMessage("foo.log", "this is a test"));

您启动了一个长时间运行的后台任务,该任务只为队列提供服务:

foreach (var msg in _logMessages.GetConsumingEnumerable())
{
    // of course you'll want your exception handling in here
    File.AppendAllText(msg.Filepath, msg.Text);
}

这里的潜在风险是线程创建消息的速度太快,导致队列无限制地增长,因为消费者跟不上。这是否是您的应用程序中的真正风险,只有您可以说。如果您认为这可能存在风险,您可以在队列中设置一个最大大小(条目数),这样如果队列大小超过该值,生产者将等到队列中有空间后才能添加。

【讨论】:

  • 谢谢,非常感谢。我喜欢我的大使用块的部分原因是我可以设置 FileShare.Read,我不确定 File.AppendAllText 的情况。但话又说回来,我想这只是我与其他进程竞争的一个因素。
  • 哦,先生,你太棒了。
  • 为什么是 BlockingCollection 而不是 ConcurrentQueue?
  • @AkmalSalikhov BlockingCollection 有一个更好的界面(尤其是GetConsumingEnumerable 方法),它比单纯的ConcurrentQueue 更易于使用。此外,它的底层数据存储可以是任何实现IProducerConsumerCollection 的东西。默认情况下,底层数据存储为ConcurrentQueue
  • 这听起来很棒!我要添加的一件事是对GetConsumingEnumerable() 的解释:如果 BlockingCollection 为空而不是返回,它实际上会阻塞。我的第一个预感是将队列服务器foreach 放入while(true) 循环中。很高兴我阅读了文档 :-)
【解决方案2】:

您也可以使用ReaderWriterLock,它被认为是在处理读写操作时控制线程安全的更“合适”的方式...

为了调试我的网络应用程序(当远程调试失败时),我使用以下命令('debug.txt' 最终位于服务器上的 \bin 文件夹中):

public static class LoggingExtensions
{
    static ReaderWriterLock locker = new ReaderWriterLock();
    public static void WriteDebug(string text)
    {
        try
        {
            locker.AcquireWriterLock(int.MaxValue); 
            System.IO.File.AppendAllLines(Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase).Replace("file:\\", ""), "debug.txt"), new[] { text });
        }
        finally
        {
            locker.ReleaseWriterLock();
        }
    }
}

希望这可以为您节省一些时间。

【讨论】:

  • 刚刚看到这篇文章,它帮助了我,谢谢@Matas!不过我想我会通知,ReaderWriterLock 有一个后继者,称为 ReaderWriterLockSlim,请参阅msdn.microsoft.com/en-us/library/…
  • ReaderWriterLock(和 ReaderWriterLockSlim)适用于需要允许多个读取器和一个写入器的情况。在这种情况下,如果您只是从多个线程写入文件而不是读取,我建议您锁定。
  • 最新更新为 ReaderWriterLockSlim, docs.microsoft.com/en-us/dotnet/api/…
猜你喜欢
  • 2012-02-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-30
相关资源
最近更新 更多