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:
- What is wrong with my implementation of the
lock()
method? - Is
ReaderWriterLockSlim
implementation any better? I have not used this before. - 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();
}
}