views:

526

answers:

7

I am using Enterprise Library 4 on one of my projects for logging (and other purposes). I've noticed that there is some cost to the logging that I am doing that I can mitigate by doing the logging on a separate thread.

The way I am doing this now is that I create a LogEntry object and then I call BeginInvoke on a delegate that calls Logger.Write.

new Action<LogEntry>(Logger.Write).BeginInvoke(le, null, null);

What I'd really like to do is add the log message to a queue and then have a single thread pulling LogEntry instances off the queue and performing the log operation. The benefit of this would be that logging is not interfering with the executing operation and not every logging operation results in a job getting thrown on the thread pool.

How can I create a shared queue that supports many writers and one reader in a thread safe way? Some examples of a queue implementation that is designed to support many writers (without causing synchronization/blocking) and a single reader would be really appreciated.

Recommendation regarding alternative approaches would also be appreciated, I am not interested in changing logging frameworks though.

A: 

An extra level of indirection may help here.

Your first async method call can put messages onto a synchonized Queue and set an event -- so the locks are happening in the thread-pool, not on your worker threads -- and then have yet another thread pulling messages off the queue when the event is raised.

Steve Gilham
A: 

If you log something on a separate thread, the message may not be written if the application crashes, which makes it rather useless.

The reason goes why you should always flush after every written entry.

leppie
That's true, I don't use this method for exception logging or other critical logging. I use it only for logging informational status messages describing successfully executed operations. These messages tend to occur much more often and I need to minimize their cost.
spoon16
+3  A: 

Yes, you need a producer/consumer queue. I have one example of this in my threading tutorial - if you look my "deadlocks / monitor methods" page you'll find the code in the second half.

There are plenty of other examples online, of course - and .NET 4.0 will ship with one in the framework too (rather more fully featured than mine!). In .NET 4.0 you'd probably wrap a ConcurrentQueue<T> in a BlockingCollection<T>.

The version on that page is non-generic (it was written a long time ago) but you'd probably want to make it generic - it would be trivial to do.

You would call Produce from each "normal" thread, and Consume from one thread, just looping round and logging whatever it consumes. It's probably easiest just to make the consumer thread a background thread, so you don't need to worry about "stopping" the queue when your app exits. That does mean there's a remote possibility of missing the final log entry though (if it's half way through writing it when the app exits) - or even more if you're producing faster than it can consume/log.

Jon Skeet
Can you link to the .NET 4 documentation describing this?
spoon16
@spoon16: Done.
Jon Skeet
thanks, appreciate the additional thoughts as well
spoon16
What is the difference between locking an object instance in the case of your ProducerConsumer example and just locking the Queue object? I'm just reading the test of your article now so I may just have not reached the explanation.
spoon16
I prefer to lock on separate objects which I absolutely *know* nothing else is going to lock on. I don't know whether Queue locks on itself internally... in this case it wouldn't matter if it did, but as a general principle, I like to keep locks "private" (so nothing else can interfere) unless I have a good reason to do otherwise.
Jon Skeet
@Jon, any comments on my ThreadedLogger class below?
Sam Saffron
@Jon, I just did a second implementation which supports dispose and uses a manual reset event. Would love to get the Skeet stamp of approval :p
Sam Saffron
A: 

If what you have in mind is a SHARED queue, then I think you are going to have to synchronize the writes to it, the pushes and the pops.

But, I still think it's worth aiming at the shared queue design. In comparison to the IO of logging and probably in comparison to the other work your app is doing, the brief amount of blocking for the pushes and the pops will probably not be significant.

Corey Trager
A: 

I suggest to start with measuring actual performance impact of logging on the overall system (i.e. by running profiler) and optionally switching to something faster like log4net (I've personally migrated to it from EntLib logging a long time ago).

If this does not work, you can try using this simple method from .NET Framework:

ThreadPool.QueueUserWorkItem

Queues a method for execution. The method executes when a thread pool thread becomes available.

MSDN Details

If this does not work either then you can resort to something like John Skeet has offered and actually code the async logging framework yourself.

Rinat Abdullin
I've already identified that there is a benefit to async logging. Also I am not in a position to switch logging frameworks as we use EntLib logging extensively. It would not be trivial or a welcome breaking change by the rest of the team :). ThreadPool.QueueUserWorkItem is basically what the BeginInvoke call that I am using now does. I think John's answer is the most appropriate so far for my situation.
spoon16
Quueing a work item does not guarantee any ordering so if you care about the order in which the messages are logged then that would not be a viable approach IMO.
Abhijeet Patel
A: 

Here is what I came up with... also see Sam Saffron's answer. This answer is community wiki in case there are any problems that people see in the code and want to update.

/// <summary>
/// A singleton queue that manages writing log entries to the different logging sources (Enterprise Library Logging) off the executing thread.
/// This queue ensures that log entries are written in the order that they were executed and that logging is only utilizing one thread (backgroundworker) at any given time.
/// </summary>
public class AsyncLoggerQueue
{
    //create singleton instance of logger queue
    public static AsyncLoggerQueue Current = new AsyncLoggerQueue();

    private static readonly object logEntryQueueLock = new object();

    private Queue<LogEntry> _LogEntryQueue = new Queue<LogEntry>();
    private BackgroundWorker _Logger = new BackgroundWorker();

    private AsyncLoggerQueue()
    {
        //configure background worker
        _Logger.WorkerSupportsCancellation = false;
        _Logger.DoWork += new DoWorkEventHandler(_Logger_DoWork);
    }

    public void Enqueue(LogEntry le)
    {
        //lock during write
        lock (logEntryQueueLock)
        {
            _LogEntryQueue.Enqueue(le);

            //while locked check to see if the BW is running, if not start it
            if (!_Logger.IsBusy)
                _Logger.RunWorkerAsync();
        }
    }

    private void _Logger_DoWork(object sender, DoWorkEventArgs e)
    {
        while (true)
        {
            LogEntry le = null;

            bool skipEmptyCheck = false;
            lock (logEntryQueueLock)
            {
                if (_LogEntryQueue.Count <= 0) //if queue is empty than BW is done
                    return;
                else if (_LogEntryQueue.Count > 1) //if greater than 1 we can skip checking to see if anything has been enqueued during the logging operation
                    skipEmptyCheck = true;

                //dequeue the LogEntry that will be written to the log
                le = _LogEntryQueue.Dequeue();
            }

            //pass LogEntry to Enterprise Library
            Logger.Write(le);

            if (skipEmptyCheck) //if LogEntryQueue.Count was > 1 before we wrote the last LogEntry we know to continue without double checking
            {
                lock (logEntryQueueLock)
                {
                    if (_LogEntryQueue.Count <= 0) //if queue is still empty than BW is done
                        return;
                }
            }
        }
    }
}
spoon16
It seems like you intend to make the AsyncLogger class a singleton. I'd make the constructor private in that case, but otherwise it seems that it gets the job done.
Abhijeet Patel
thanks, good catch
spoon16
+6  A: 

I wrote this code a while back, feel free to use it.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace MediaBrowser.Library.Logging {
    public abstract class ThreadedLogger : LoggerBase {

        Queue<Action> queue = new Queue<Action>();
        AutoResetEvent hasNewItems = new AutoResetEvent(false);
        volatile bool waiting = false;

        public ThreadedLogger() : base() {
            Thread loggingThread = new Thread(new ThreadStart(ProcessQueue));
            loggingThread.IsBackground = true;
            loggingThread.Start();
        }


        void ProcessQueue() {
            while (true) {
                waiting = true;
                hasNewItems.WaitOne(10000,true);
                waiting = false;

                Queue<Action> queueCopy;
                lock (queue) {
                    queueCopy = new Queue<Action>(queue);
                    queue.Clear();
                }

                foreach (var log in queueCopy) {
                    log();
                }
            }
        }

        public override void LogMessage(LogRow row) {
            lock (queue) {
                queue.Enqueue(() => AsyncLogMessage(row));
            }
            hasNewItems.Set();
        }

        protected abstract void AsyncLogMessage(LogRow row);


        public override void Flush() {
            while (!waiting) {
                Thread.Sleep(1);
            }
        }
    }
}

Some advantages:

  • It keeps the background logger alive, so it does not need to spin up and spin down threads.
  • It uses a single thread to service the queue, which means there will never be a situation where 100 threads are servicing the queue.
  • It copies the queues to ensure the queue is not blocked while the log operation is performed
  • It uses an AutoResetEvent to ensure the bg thread is in a wait state
  • It is, IMHO, very easy to follow

Here is a slightly improved version, keep in mind I performed very little testing on it, but it does address a few minor issues.

public abstract class ThreadedLogger : IDisposable {

    Queue<Action> queue = new Queue<Action>();
    ManualResetEvent hasNewItems = new ManualResetEvent(false);
    ManualResetEvent terminate = new ManualResetEvent(false);
    ManualResetEvent waiting = new ManualResetEvent(false);

    Thread loggingThread; 

    public ThreadedLogger() {
        loggingThread = new Thread(new ThreadStart(ProcessQueue));
        loggingThread.IsBackground = true;
        // this is performed from a bg thread, to ensure the queue is serviced from a single thread
        loggingThread.Start();
    }


    void ProcessQueue() {
        while (true) {
            waiting.Set();
            int i = ManualResetEvent.WaitAny(new WaitHandle[] { hasNewItems, terminate });
            // terminate was signaled 
            if (i == 1) return; 
            hasNewItems.Reset();
            waiting.Reset();

            Queue<Action> queueCopy;
            lock (queue) {
                queueCopy = new Queue<Action>(queue);
                queue.Clear();
            }

            foreach (var log in queueCopy) {
                log();
            }    
        }
    }

    public void LogMessage(LogRow row) {
        lock (queue) {
            queue.Enqueue(() => AsyncLogMessage(row));
        }
        hasNewItems.Set();
    }

    protected abstract void AsyncLogMessage(LogRow row);


    public void Flush() {
        waiting.WaitOne();
    }


    public void Dispose() {
        terminate.Set();
        loggingThread.Join();
    }
}

Advantages over the original:

  • It's disposable, so you can get rid of the async logger
  • The flush semantics are improved
  • It will respond slightly better to a burst followed by silence
Sam Saffron
Your "waiting" flag should be volatile if it's being used from multiple threads - or use the lock when accessing it. I'm not sure I see the point in waiting for 10 seconds rather than just waiting without a timeout. I'd also call Set while you still hold the lock - it won't actually make much difference, but you could get two threads both adding events, then one thread setting the event, the reader completing the wait and copying the data, *then* the other writer setting the event again. No great loss, admittedly. Why do you have the loggingThread instance variable?
Jon Skeet
The 10 second wait, is to avoid a starvation, in the case where you get a burst of events that is partially serviced. (getting events just after the queue is copied) I should probably change this to a manual reset event, and perhaps add some logic to re-service the queue until its really empty, before waiting again. The thread var is just a tiny bit of future proofing that isn't really needed (in case I want to get the id of the thread or something).
Sam Saffron
amended the code to account for the comments by Jon, Thanks!
Sam Saffron
Is there really a benefit by using a Thread object instead of spinning up a BackgroundWorker over and over? Isn't the BackgroundWorker just throwing jobs on the ThreadPool... which should manage the thread sharing/creation for you.
spoon16
@spoon, yerp there is an advantage, by using a single thread your more elegantly serialize the access, in a thread pool scenario you could end up having 100 threads servicing the queue which is not desirable
Sam Saffron
If you have only one BackgroundWorker running at any one time though you never actually have more than one thread working on the queue and you don't have to explicitly manage the thread associated with the BackgroundWorker. Right?
spoon16
true, but you will have to manage the lifetime of a single background worker, its really not the tool for the job cause you have no need for the progress facilities etc that come with it.
Sam Saffron
Before the hasNewItems.Reset(); it might be a good idea to put in a small delay, like 10 milleconds to allow the queue to build up some data.
FlappySocks