Jenninha
Jenninha

Reputation: 1377

Lock text file during read and write or alternative

I have an application where I need to create files with a unique and sequential number as part of the file name. My first thought was to use (since this application does not have any other data storage) a text file that would contain a number and I would increment this number so then my application would always create a file with a unique id. Then I thought that maybe at a time when there are more than one user submitting to this application at the same time, one process might be reading the txt file before it has been written by the previous process. So then I am looking for a way to read and write to a file (with try catch so then I can know when it's being used by another process and then wait and try to read from it a few other times) in the same 'process' without unlocking the file in between. If what I am saying above sounds like a bad option, could you please give me an alternative to this? How would you then keep track of unique identification numbers for an application like my case?

Thanks.

Upvotes: 0

Views: 695

Answers (3)

Zverev Evgeniy
Zverev Evgeniy

Reputation: 3712

I would prefer a complex and universal solution with the global mutex. It uses a mutex with name prefixed with "Global\" which makes it system-wide i.e. one mutex instance is shared across all processes. if your program runs in friendly environment or you can specify strict permissions limited to a user account you can trust then it works well.

Keep in mind that this solution is not transactional and is not protected against thread-abortion/process-termination.

Not transactional means that if your process/thread is caught in the middle of storage file modification and is terminated/aborted then the storage file will be left in unknown state. For instance it can be left empty. You can protect yourself against loss of data (loss of last used index) by writing the new value first, saving the file and only then removing the previous value. Reading procedure should expect a file with multiple numbers and should take the greatest.

Not protected against thread-abortion means that if a thread which obtained the mutex is aborted unexpectedly and/or you do not have proper exception handling then the mutex could stay locked for the life of the process that created that thread. In order to make solution abort-protected you will have to implement timeouts on obtaining the lock i.e. replace the following line which waits forever

blnResult = iLock.Mutex.WaitOne();

with something with timeout.

Summing this up I try to say that if you are looking for a really robust solution you will come to utilizing some kind of a transactional database or write a kind of such a database yourself :)

Here is the working code without timeout handling (I do not need it in my solution). It is robust enough to begin with.

using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Threading;

namespace ConsoleApplication31
{
    class Program
    {
        //You only need one instance of that Mutex for each application domain (commonly each process).
        private static SMutex mclsIOLock;

        static void Main(string[] args)
        {
            //Initialize the mutex. Here you need to know the path to the file you use to store application data.
            string strEnumStorageFilePath = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                "MyAppEnumStorage.txt");
            mclsIOLock = IOMutexGet(strEnumStorageFilePath);
        }

        //Template for the main processing routine.
        public static void RequestProcess()
        {
            //This flag is used to protect against unwanted lock releases in case of recursive routines.
            bool blnLockIsSet = false;

            try
            {
                //Obtain the lock.
                blnLockIsSet = IOLockSet(mclsIOLock);

                //Read file data, update file data. Do not put much of long-running code here.
                //Other processes may be waiting for the lock release.
            }
            finally
            {
                //Release the lock if it was obtained in this particular call stack frame.
                IOLockRelease(mclsIOLock, blnLockIsSet);
            }

            //Put your long-running code here.
        }

        private static SMutex IOMutexGet(string iMutexNameBase)
        {
            SMutex clsResult = null;
            clsResult = new SMutex();

            string strSystemObjectName = @"Global\" + iMutexNameBase.Replace('\\', '_');

            //Give permissions to all authenticated users.
            SecurityIdentifier clsAuthenticatedUsers = new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null);
            MutexSecurity clsMutexSecurity = new MutexSecurity();
            MutexAccessRule clsMutexAccessRule = new MutexAccessRule(
                clsAuthenticatedUsers,
                MutexRights.FullControl,
                AccessControlType.Allow);
            clsMutexSecurity.AddAccessRule(clsMutexAccessRule);

            //Create the mutex or open an existing one.
            bool blnCreatedNew;
            clsResult.Mutex = new Mutex(
                false,
                strSystemObjectName,
                out blnCreatedNew,
                clsMutexSecurity);
            clsResult.IsMutexHeldByCurrentAppDomain = false;

            return clsResult;
        }

        //Release IO lock.
        private static void IOLockRelease(
            SMutex iLock,
            bool? iLockIsSetInCurrentStackFrame = null)
        {
            if (iLock != null)
            {
                lock (iLock)
                {
                    if (iLock.IsMutexHeldByCurrentAppDomain &&
                        (!iLockIsSetInCurrentStackFrame.HasValue ||
                        iLockIsSetInCurrentStackFrame.Value))
                    {
                        iLock.MutexOwnerThread = null;
                        iLock.IsMutexHeldByCurrentAppDomain = false;
                        iLock.Mutex.ReleaseMutex();
                    }
                }
            }
        }

        //Set the IO lock.
        private static bool IOLockSet(SMutex iLock)
        {
            bool blnResult = false;

            try
            {
                if (iLock != null)
                {
                    if (iLock.MutexOwnerThread != Thread.CurrentThread)
                    {
                        blnResult = iLock.Mutex.WaitOne();
                        iLock.IsMutexHeldByCurrentAppDomain = blnResult;

                        if (blnResult)
                        {
                            iLock.MutexOwnerThread = Thread.CurrentThread;
                        }
                        else
                        {
                            throw new ApplicationException("Failed to obtain the IO lock.");
                        }
                    }
                }
            }
            catch (AbandonedMutexException iMutexAbandonedException)
            {
                blnResult = true;
                iLock.IsMutexHeldByCurrentAppDomain = true;
                iLock.MutexOwnerThread = Thread.CurrentThread;
            }

            return blnResult;
        }
    }

    internal class SMutex
    {
        public Mutex Mutex;
        public bool IsMutexHeldByCurrentAppDomain;
        public Thread MutexOwnerThread;
    }
}

Upvotes: 0

Jim Mischel
Jim Mischel

Reputation: 133950

If it's a single application then you can store the current number in your application settings. Load that number at startup. Then with each request you can safely increment it and use the result. Save the sequential number when the program shuts down. For example:

private int _fileNumber;

// at application startup
_fileNumber = LoadFileNumberFromSettings();

// to increment
public int GetNextFile()
{
    return Interlocked.Increment(ref _fileNumber);
}

// at application shutdown
SaveFileNumberToSettings(_fileNumber);

Or, you might want to make sure that the file number is saved whenever it's incremented. If so, change your GetNextFile method:

private readonly object _fileLock = new object();
public int GetNextFile()
{
    lock (_fileLock)
    {
        int result = ++_fileNumber;
        SaveFileNumbertoSettings(_fileNumber);
        return result;
    }
}

Note also that it might be reasonable to use the registry for this, rather than a file.

Upvotes: 1

Shimrod
Shimrod

Reputation: 3205

Edit: As Alireza pointed in the comments, it is not a valid way to lock between multiple applications.

You can always lock the access to the file (so you won't need to rely on exceptions). e.g:

// Create a lock in your class
private static object LockObject = new object();

// and then lock on this object when you access the file like this:
lock(LockObject)
{
    ... access to the file
}

Edit2: It seems that you can use Mutex to perform inter-application signalling.

private static System.Threading.Mutex m = new System.Threading.Mutex(false, "LockMutex");

void AccessMethod()
{
    try
    {
        m.WaitOne();
        // Access the file

    }
    finally
    {
        m.ReleaseMutex();
    }
}

But it's not the best pattern to generate unique ids. Maybe a sequence in a database would be better ? If you don't have a database, you can use Guids or a local database (even Access would be better I think)

Upvotes: 0

Related Questions