以线程安全的方式写入文件

时间:2014-05-07 15:38:49

标签: c# multithreading thread-safety filesystems

异步写入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();
    }

2 个答案:

答案 0 :(得分:38)

你如何接近这将取决于你写作的频率。如果您很少写入相对少量的文本,那么只需使用静态锁并完成它。在任何情况下,这可能是您最好的选择,因为磁盘驱动器一次只能满足一个请求。假设您的所有输出文件都在同一个驱动器上(可能不是一个公平的假设,但请耐心等待),在应用程序级别锁定与在操作系统级别完成的锁定之间没有太大区别。

因此,如果您将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);
}

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

答案 1 :(得分:16)

你也可以使用ReaderWriterLock,它被认为是更合适的&#39;在处理读写操作时控制线程安全的方法......

要调试我的网络应用程序(当远程调试失败时),我使用以下内容(&#39; debug.txt&#39;最终在服务器上的\ 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();
        }
    }
}

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