0

I have a class that programmatically creates a log using the Nlog framework. I have several processes running and creating their logs at the same time. I added a lock to the constructor of the class, as before the two threads were attempting to create a file at the same time which resulted in some annoying bugs (like only creating one log).

This seems to have solved that problem. However now i have the same issue with writing to the log, and using a lock has not helped. Here is the class.

public class CwiLogger
    {
        private Logger _log;
        private static Object createLogLock = new Object();
        private static Object writeLogLock = new Object();
        public CwiLogger(string logPath, string logName, string className)
        {
            lock (createLogLock)
            {
                var config = new LoggingConfiguration();
                var fileTarget = new FileTarget();
                config.AddTarget("file", fileTarget);
                fileTarget.FileName = Path.Combine(logPath, logName);
                fileTarget.Layout = "${longdate}|${level:uppercase=true}|${logger}|${message}";
                var rule = new LoggingRule("*", LogLevel.Debug, fileTarget);
                config.LoggingRules.Add(rule);

                LogManager.Configuration = config;

                this._log = LogManager.GetLogger(className);
            }
        }

        public void AddToLog(string logText, LogLevel level = null)
        {
            lock (writeLogLock)
            {
                level = level ?? LogLevel.Info;
                this._log.Log(level, logText + "\n");
            }
        }
    }

In my client code i run two threads each running a process that looks like this:

var log = new CwiLogger(@"C:\Users\jma\Documents\ProgrammingJunk\logTest", "Log2.txt", "Log2");
for (int i = 0; i < 100; i++)
{
    log.AddToLog("Log2 " + i);
}

only i use log1 for one and log2 for the other.

in my output. One of the 2 logs always sucessfully counts to 99, while the other gets 4-5 in and then has no other output.

Jacob Alley
  • 767
  • 8
  • 34
  • You are locking on different objects. Shouldn't they be the same? – vyrp Mar 30 '17 at 18:24
  • You don't need a lock in AddToLog method since it will operate on two different files. – Dmitriy Mar 30 '17 at 18:26
  • I'am not sure that the `LogManager.Configuration` should be set many time. – Kalten Mar 30 '17 at 18:30
  • @vyrp, i am lockign a static lock so that is done at the type level – Jacob Alley Mar 30 '17 at 18:39
  • @ Dmitry, you would think so, but for whatever reason it doesnt work with no lock (or with a lock) – Jacob Alley Mar 30 '17 at 18:40
  • @Kalten can you please explain – Jacob Alley Mar 30 '17 at 18:40
  • This might be useful: [Log4Net: Programmatically specify multiple loggers (with multiple file appenders)](http://stackoverflow.com/questions/308436/log4net-programmatically-specify-multiple-loggers-with-multiple-file-appenders). – Dustin Kingen Mar 30 '17 at 18:42
  • The problem is your assignment LogManager.Configuration = config. This will discard the old configuration (Along with all your previous FileTargets). Why not have a single FileTarget with FileName = "D:\Log\{logger:shortname=true}.txt". Then you just need to call LogManager.GetLogger(name) to setup a new logger. And multiple threads can reuse the same logger-name without needing any lock-objects. – Rolf Kristensen Mar 30 '17 at 19:12
  • @RolfKristensen That's what I thought about. To test that, create the 2 instances of CiwLogger and then run the 2 for loop. You should see that all log goes to the 2nd CwiLogger. No need to use different thread. – Kalten Mar 30 '17 at 19:20
  • well that test is basically what i am doing. I do not want all the logging to go to the 2nd logger though, i want it to go to whichever the correct logger should be....and @RolfKristensen what is the name value that you are passing into GetLogger? – Jacob Alley Mar 30 '17 at 19:22
  • @JacobAlley That would be the "logName" parameter. – Rolf Kristensen Mar 30 '17 at 19:34
  • simply getting rid of the LogManager.Configuration = config call and changing the GetLogger call does not even create either log – Jacob Alley Mar 30 '17 at 19:43
  • https://github.com/NLog/NLog/wiki/Tutorial NLog loggers are already thread-safe. You don't need to lock around writing to the logger. (See "Creating Loggers" last paragraph). You are just introducing the potential of a deadlock. – Berin Loritsch Mar 30 '17 at 20:00
  • well then why is it failing to create 2 separate log files when i do not have the lock, but when i do have it, it creates the separate files as desired? – Jacob Alley Mar 30 '17 at 20:03
  • I'm saying you don't need the `writeLogLock`. As to the creation bit, the only thing that is spoken of as thread-safe is the `Logger` instance itself. – Berin Loritsch Mar 30 '17 at 20:05
  • You're overriding the rule for the first log you're creating. You need to add the rules from first log during the creation of the second one. – VMAtm Mar 31 '17 at 04:32

2 Answers2

1

This is because the lock() syntax compiles to Monitor.Enter and Monitor.Leave - Monitor.Enter will put the current thread to sleep if it cannot lock the object, the thread holding the lock and will then awake all waiting threads when it calls Monitor.Leave.

Your problem is that your second thread has to go through the additional time of waking up before it can attempt to lock the object, by which time the thread that just released the lock has already locked it again.

If you expect your routine to lock only for a very short time (too short to want to sleep waiting threads) use a SpinLock instead. This class gets each thread to keep attempting the lock in a loop until it succeeds rather than sleeping. It means it uses more CPU (and thus more battery power on a laptop), which is why the code you lock must run for very small amounts of time.

Peter Morris
  • 20,174
  • 9
  • 81
  • 146
1

Maybe something like this:

public class CwiLogger
{
    private static LoggingConfiguration _logConfig = new LoggingConfiguration();
    private static Object createLogLock = new Object();
    private Logger _log;

    public CwiLogger(string logPath, string logName, string className)
    {
            lock (createLogLock)
            {
                var fileTarget = _logConfig.FindTargetByName(logName);
                if (fileTarget == null)
                {
                    var fileTarget = new FileTarget(logName);
                    fileTarget.FileName = Path.Combine(logPath, logName);
                    fileTarget.Layout = "${longdate}|${level:uppercase=true}|${logger}|${message}";
                    _logConfig.AddTarget(fileTarget);
                    var rule = new LoggingRule(className, LogLevel.Debug, fileTarget) { Final = true };
                    _logConfig.LoggingRules.Add(rule);
                    LogManager.Configuration = _logConfig;
                }
            }
        this._log = LogManager.GetLogger(className);
    }

    public void AddToLog(string logText, LogLevel level = null)
    {
        level = level ?? LogLevel.Info;
        this._log.Log(level, logText + "\n");
    }
}

Or maybe steal some ideas from here: https://github.com/NLog/NLog/issues/1998

Rolf Kristensen
  • 17,785
  • 1
  • 51
  • 70
  • Sorry i had to leave yesterday but this works! Thank you! So what is different about this one? the fact that I have a static LoggingConfig as well as a static lock. That way I am not overwriting the log config every time a new log is created? – Jacob Alley Mar 31 '17 at 12:32
  • @JacobAlley The difference is that this example appends to the existing LoggingConfiguration, instead of creating a new one and discarding the previous one. – Rolf Kristensen Mar 31 '17 at 17:33