Home > Software design >  Process and Thread Safe way to write to multiple files
Process and Thread Safe way to write to multiple files

Time:08-04

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:\MySite\App_Data\2022-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);
    }
}

CodePudding user response:

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, "Global\MyLoggingFileMutex");

    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.

CodePudding user response:

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)

public static void WriteToFileProcessAndThreadSafe(string filePath, string fileName, string logText)
{
    //The backslash (\) is a reserved character in a mutex name.
    //The name can be no more than 260 characters in length.
    Mutex LogWriteLocker = new Mutex(false, "Global\\"   fileName);

    try
    {
        try
        {
            LogWriteLocker.WaitOne(3000); //wait up to 3 seconds
        }
        catch(AbandonedMutexException amex)
        {
            //Mutex abandoned in another thread
        }

        //We are now thread and process safe. Do the work.
        File.AppendAllText(Path.Combine(filePath, fileName), logText);
    }
    catch (Exception exx)
    {
        //Error writing to file
    }
    finally
    {
        LogWriteLocker.ReleaseMutex();
    }
}
  • Related