Запись в файл в потоковом режиме

Написание Stringbuilder для асинхронного файла. Этот код управляет файлом, записывает в него поток и освобождает его. Он касается запросов от асинхронных операций, которые могут появиться в любое время.

FilePath устанавливается для экземпляров класса (так что объект блокировки для каждого экземпляра), но есть вероятность конфликта, поскольку эти классы могут совместно использовать FilePaths. Такой конфликт, как и все другие типы извне класса экземпляра, будет рассматриваться с повторениями.

Является ли этот код подходящим для его цели? Есть ли лучший способ справиться с этим, что означает меньшую (или нет) зависимость от механика catch и retry?

Также как избежать исключения исключений, возникших по другим причинам.

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();
    }

Ответы

Ответ 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);
}

Ваш потенциальный риск здесь заключается в том, что потоки создают сообщения слишком быстро, заставляя очередь расти без ограничений, потому что потребитель не может идти в ногу. Является ли это реальным риском в вашем приложении, вы только можете сказать. Если вы считаете, что это может быть рискован, вы можете поместить максимальный размер (количество записей) в очередь, чтобы, если размер очереди превышает это значение, производители будут ждать, пока в очереди не будет места, прежде чем они смогут добавить.

Ответ 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();
        }
    }
}

Надеюсь, это сэкономит вам некоторое время.