Асинхронный регистратор. Могу ли я потерять/задержать записи журнала?

Я реализую свою собственную структуру ведения журнала. Ниже приведен мой BaseLogger, который получает записи журнала и отправляет их в фактический Logger, который реализует метод abstract Log.

Я использую C# TPL для асинхронного ведения журнала. Я использую потоки вместо TPL. (Задача TPL не содержит реального потока. Поэтому, если все потоки приложения завершатся, задачи также остановятся, что приведет к потере всех «ожидающих» записей журнала.)

public abstract class BaseLogger
{
    // ... Omitted properties constructor .etc. ... //

public virtual void AddLogEntry(LogEntry entry)
{
    if (!AsyncSupported)
    {
        // the underlying logger doesn't support Async.
        // Simply call the log method and return.
        Log(entry);
        return;
    }
    // Logger supports Async.
    LogAsync(entry);
}

private void LogAsync(LogEntry entry)
{
    lock (LogQueueSyncRoot) // Make sure we ave a lock before accessing the queue.
    {
        LogQueue.Enqueue(entry);
    }

    if (LogThread == null || LogThread.ThreadState == ThreadState.Stopped)
    { // either the thread is completed, or this is the first time we're logging to this logger.
        LogTask = new  new Thread(new ThreadStart(() =>
        {
            while (true)
            {
                LogEntry logEntry;
                lock (LogQueueSyncRoot)
                {
                    if (LogQueue.Count > 0)
                    {
                        logEntry = LogQueue.Dequeue();
                    }
                    else
                    {
                        break; 
                        // is it possible for a message to be added,
                        // right after the break and I leanve the lock {} but 
                        // before I exit the loop and task gets 'completed' ??
                    }
                }
                Log(logEntry);
             }
        }));
        LogThread.Start();
    }
}

// Actual logger implimentations will impliment this method.
protected abstract void Log(LogEntry entry);
}

Обратите внимание, что AddLogEntry можно вызывать из нескольких потоков одновременно.

Мой вопрос: возможно ли, чтобы эта реализация теряла записи журнала? Меня беспокоит, можно ли добавить запись журнала в очередь, сразу после того, как мой поток существует в цикле с оператором break и выходит из блока блокировки, который находится в предложении else, а поток все еще находится в Состояние «Работает».

Я понимаю, что, поскольку я использую очередь, даже если я пропущу запись, следующий запрос в журнал также подтолкнет пропущенную запись. Но это неприемлемо, особенно если это происходит для последней записи журнала приложения.

Кроме того, пожалуйста, дайте мне знать, могу ли я реализовать то же самое, но с использованием новых ключевых слов C# 5.0 async и await с более чистым кодом. Я не против требовать .NET 4.5.

Заранее спасибо.


person Madushan    schedule 25.08.2012    source источник
comment
Ах... к чёрту. Здесь полночь. Я копаю это и использую NLog XD ... Всем спасибо за помощь.   -  person Madushan    schedule 25.08.2012


Ответы (3)


Я сразу вижу два состояния гонки:

  1. Вы можете запустить более одного Thread, если несколько потоков вызывают AddLogEntry. Это не приведет к потере событий, но неэффективно.
  2. Да, событие может быть поставлено в очередь во время выхода из Thread, и в этом случае оно будет "потеряно".

Кроме того, здесь есть серьезная проблема с производительностью: если вы не ведете журнал постоянно (тысячи раз в секунду), вы будете запускать новый Thread для каждой записи в журнале. Это быстро подорожает.

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

Тем не менее, если вам нужен хороший подход на основе .NET 4.5, это довольно просто:

public abstract class BaseLogger
{
  private readonly ActionBlock<LogEntry> block;

  protected BaseLogger(int maxDegreeOfParallelism = 1)
  {
    block = new ActionBlock<LogEntry>(
        entry =>
        {
          Log(entry);
        },
        new ExecutionDataflowBlockOptions
        {
          MaxDegreeOfParallelism = maxDegreeOfParallelism,
        });
  }

  public virtual void AddLogEntry(LogEntry entry)
  {
    block.Post(entry);
  }

  protected abstract void Log(LogEntry entry);
}
person Stephen Cleary    schedule 25.08.2012

Хотя вы, вероятно, могли бы заставить это работать, по моему опыту, я бы рекомендовал, если это возможно, использовать существующую структуру ведения журнала :) Например, существуют различные варианты асинхронного ведения журнала/добавления с log4net, такие как эта обертка асинхронного приложения.

В противном случае, ИМХО, поскольку вы все равно будете блокировать поток пула потоков во время операции ведения журнала, я бы вместо этого просто запустил выделенный поток для вашего ведения журнала. Кажется, вы уже используете этот подход, просто через Task, чтобы не удерживать поток пула потоков, когда ничего не регистрируется. Тем не менее, я думаю, что упрощение в реализации приносит пользу только при наличии выделенного потока.

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

Имея выделенный поток как единственное, что записывает, исключается любая возможность того, что несколько потоков/задач будут получать записи из очереди и пытаться одновременно записывать записи в журнал (болезненное состояние гонки). Поскольку метод журнала теперь просто добавляется в коллекцию, он не должен быть асинхронным, и вам вообще не нужно иметь дело с TPL, что делает его более простым и легким для рассуждений (и, надеюсь, в категории « очевидно правильно» или около того :)

Этот подход «выделенного потока ведения журнала» - это то, что, как я полагаю, делает приложение log4net, с которым я связался, FWIW, в случае, если это помогает служить примером.

person James Manning    schedule 25.08.2012
comment
Итак.... возможно ли, чтобы моя реализация пропустила записи в журнале? указанным образом, между break и выполнением задачи? - person Madushan; 25.08.2012
comment
Обратите внимание. Я обновил код. Я использую Threads, как вы предложили, потому что он становится неприятным и теряет «ожидающие» записи, когда приложение заканчивается. - person Madushan; 25.08.2012

Что касается потери ожидающих сообщений о сбое приложения из-за необработанного исключения, я привязал обработчик к событию AppDomain.CurrentDomain.DomainUnload. Выходит так:

protected ManualResetEvent flushing = new ManualResetEvent(true);
protected AsyncLogger()  // ctor of logger
{
    AppDomain.CurrentDomain.DomainUnload += CurrentDomain_DomainUnload;
}

protected void CurrentDomain_DomainUnload(object sender, EventArgs e)
{
    if (!IsEmpty)
    {
        flushing.WaitOne();
    }
}

Может быть, не слишком чистый, но работает.

person Arthur    schedule 18.06.2013