skip to Main Content

I have an .ASMX API build using c#/asp.net/.net4.7.2 and it has been set to use 16 threads in IIS. There are various modules that write (only writing, no reading) to multiple log files like so:

2022-06-01-Wed-_ModuleA.log
2022-06-01-Wed-_ModuleB.log

I was using the usual lock() method:

public class Audit {
    public static object LogWriteLocker = new object();
    public void LogEntry(string path, string logText) {
        lock (LogWriteLocker) {
             File.AppendAllText(path, logText);
        }
    }
}

This should not be happening, but I was seeing errors like:

The process cannot access the file 'D:MySiteApp_Data2022-06-01-Wed-_ModuleA.log' because it is being used by another process.

So I am trying to figure our a workaround like below:

readonly private static ReaderWriterLockSlim _readWriteLock = new ReaderWriterLockSlim();
public static void WriteToFileThreadSafe(string path, string text) {
    _readWriteLock.EnterWriteLock();
    try {
        File.AppendAllText(path, text);
    }
    catch (Exception exx) {
        LogException(exx, "Error Writing to Log");
    }
    finally {
        _readWriteLock.ExitWriteLock();
    }
}

Sorry if there is TMI, but my questions are:

  1. What is wrong with my implementation of the lock() method?
  2. Is ReaderWriterLockSlim implementation any better? I have not used this before.
  3. One problem is using a single lock object for multiple files, where as I should have an array of lock objects/ReaderWriterLockSlim. The number of log files are dynamic so how do I do that efficiently?

Thank you.

Edit:

Contemplating a 3rd option, that has separate locks for each file path:

private static ConcurrentDictionary<string, object> LogWriteLocker = new ConcurrentDictionary<string, object>();
public static void WriteToFileThreadSafe(string path, string text)
{
    //Adds a key/value pair to the ConcurrentDictionary<TKey,TValue> if the key does not already exist. Returns the new value, or the existing value if the key already exists.
    var lockObj = LogWriteLocker.GetOrAdd(path, new object());
    lock(lockObj)
    {
        File.AppendAllText(path, text);
    }
}

2

Answers


  1. Chosen as BEST ANSWER

    In my project I had about 10 processes that were writing to some common logs. I kept getting the UnauthorizedAccessException: Access to the path 'Globalmymutexid' is denied. intermittently. So I ended up changing the constructor as below (idea from here) :

    public static bool WriteToFileProcessAndThreadSafe(string filePath, string fileName, string logText)
    {
        //Mutex LogWriteLocker = new Mutex(false, "Global\" + fileName); // causes UnauthorizedAccessException
        Mutex LogWriteLocker = null;
    
        try
        {
            MutexSecurity mutexSecurity = new MutexSecurity();
            mutexSecurity.AddAccessRule(new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.Synchronize | MutexRights.Modify, AccessControlType.Allow));
    
            // attempt to create the mutex, with the desired DACL..
            //The backslash () is a reserved character in a mutex name.
            //The name can be no more than 260 characters in length.
            LogWriteLocker = new Mutex(false, "Global\" + fileName, out _, mutexSecurity);
    
            try
            {
                LogWriteLocker.WaitOne();
            }
            catch (AbandonedMutexException amex)
            {
                //Handle AbandonedMutexException;
            }
    
            //We are now thread and process safe. Do the work.
            File.AppendAllText(Path.Combine(filePath, fileName), logText);
            
            return true;
        }
        catch (WaitHandleCannotBeOpenedException)
        {
            // the mutex cannot be opened, probably because a Win32 object of a different
            // type with the same name already exists.
            return false;
        }
        catch (UnauthorizedAccessException)
        {
            // the mutex exists, but the current process or thread token does not
            // have permission to open the mutex with SYNCHRONIZE | MUTEX_MODIFY rights.
            return false;
        }
        catch (Exception exx)
        {
            //Handle other exceptions
            return false;
        }
        finally
        {
            if(LogWriteLocker != null) 
            {
                LogWriteLocker.ReleaseMutex();
                LogWriteLocker.Dispose();
            }
        }        
    }
    

    I have finally understood that my mistake was thinking of this in terms of "milti-threading" were as the issue was due to "multi-process". All the variations I have listed in the OP end up with lock() at the core, which works only for single process, multi thread situation.

    Thanks to @Eldar and just an addition to @Charlieface answer, here is what I am using for my cause. I use the fileName as mutex name so that each file has a separate lock (as opposed to using one lock for all files)


  2. Looks like you are writing to the file across different processes. In which case you need a global mutex.

    public class Audit
    {
        public static Mutex LogWriteLocker = new Mutex(false, "GlobalMyLoggingFileMutex");
    
        public void LogEntry(string path, string logText)
        {
            try
            {
                try
                {
                    LogWriteLocker.WaitOne();
                }
                catch (AbandonedMutexException)
                { //
                }
                File.AppendAllText(path, logText);
            }
            finally
            {
                LogWriteLocker.ReleaseMutex();
            }
        }
    }
    

    The ReleaseMutex call must happen on the same thread as WaitOne, so if you use async you need to make sure to marshal back to the original thread using a SynchonizationContext.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search