How to lock a text file for reading and writing using C#

If you have multiple threads or processes accessing the same text file, each thread or process will need: a) to lock the text file so no other thread or process can access it, b) read and write from it and c) unlock it, for another thread or process to access it. I am going to show you how to do that using C# and how to test that your program is correct.

First I am going to provide a program that uses the technique I described above in order to safely access a text file for reading and writing. This program, called “LabLock.cs”, uses .NET framework v2.0 and shows the complete solution to the problem of safely accessing a text file. Here it is:

using System;
using System.IO;
using System.Threading;

class LabLock
{
    public static void Main()
    {

        string myFile = @"C:\Documents and Settings\user\Desktop\TheFile.txt";
        FileStream myFileStream = null;
        System.Collections.Generic.List<string> myList = new System.Collections.Generic.List<string>();
        string myLine;

        Console.WriteLine("Sequence begins.");

        try
        {
           while (true)
           {
              try
              {
                 myFileStream = new FileStream(myFile, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
                 break;
              }
              catch
              {
                 Thread.Sleep(20);
              }
           }

           StreamReader myStreamReader = new StreamReader(myFileStream);
           StreamWriter myStreamWriter = new StreamWriter(myFileStream);

           while ((myLine = myStreamReader.ReadLine()) != null)
           {
              myList.Add(myLine);
           }

           myFileStream.Seek(0, SeekOrigin.Begin);
           myFileStream.SetLength(0);

           myStreamWriter.WriteLine("One");
           myStreamWriter.WriteLine("Two");

           myStreamWriter.Close();
           myStreamReader.Close();
           myFileStream.Close();
           myFileStream.Dispose();
        }
        finally
        {
           if (myFileStream != null)
           {
              myFileStream.Dispose();
           }
        }

        Console.WriteLine("Sequence ended.");
        Console.WriteLine("Press Enter to exit.");
        Console.ReadLine();

    }
}

You can compile LabLock.cs using the following batch file, which produces LabLock.exe.

del LabLock.exe

%systemroot%\Microsoft.NET\Framework\v2.0.50727\csc LabLock.cs

pause

Be sure to put the batch file in the same directory as LabLock.cs. Also, be sure to change the string myFile in the beginning of LabLock.cs to point to your file. You must use this method each time a thread or process wants to access the file. That is it. But how does it work and how can you test it?

When the program starts, it uses a while loop

while (true)
{
   try
   {
      myFileStream = new FileStream(myFile, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
      break;
   }
   catch
   {
      Thread.Sleep(20);
   }
}

to wait indefinitely for any other thread or process that accesses the file. The while loop tries to create a new FileStream in order to access the file, and if it fails, sleeps for 20 milliseconds, an interval chosen by me arbitrarily, and then tries again. When the file can be accessed safely, “new Filestream” will succeed in creating a FileStream with the parameters passed to it: The access will be for reading and writing and no other thread or process will be able to access.

Usually, when we access a file we use a StreamReader or a StreamWriter from the beginning,  passing them the name of the file. They implicitly create a FileStream, but we do not have to know anything about it. But in this case, we have to go “low level”: Because it is the FileStream’s FileShare.None parameter that will provide the safe access to the file and we have to explicitly ask for it, we have to start by first creating the FileStream ourselves. Then, when it is time to acces the file, we will still create a StreamReader and a StreamWriter, but we will pass the FileStream to them instead of the name of the file.

So, great objects those StreamReaders and StreamWriters. They have more than one constructor, so that you can either instantiate them by giving them a file and letting them create the FileStream object, or you can create the FileStream object that you specifically want and instantiate them by giving them this FileStream object.

As you can see, I use a StreamReader to read the file, then I use these two instructions

myFileStream.Seek(0, SeekOrigin.Begin);
myFileStream.SetLength(0);

and then I use a StreamWriter to write the file. Also, please notice that I write only a few things to the file instead of the whole file back, which, depending on the contents of the file would have been more data than a few characters. I write to the file

myStreamWriter.WriteLine("One");
myStreamWriter.WriteLine("Two");

instead of something like

foreach (string myLineString in myList)
{
   myStreamWriter.WriteLine(myLineString + "new data");
}

in order to test what happens if I write less stuff than what I had before.

Ok, let’s see why I did things this way, that is, let’s see why I used the Seek and SetLength instructions. There is a caveat when using FileStream the way I used it in this program. If you use the StreamReader to read the  file, the FileStream is not reset. So, when it comes time to use the StreamWriter to write the file, the StreamWriter (which uses the same FileStream) will start writing from where the StreamReader stopped reading. This may be great for inserting or appending, but it is bad for overwriting purposes. Thus we have to use the Seek instruction to make the FileStream point to the beginning of the file after the reading action and we have to use the SetLength instruction to clear the contents of the FileStream. This way, we can read everything from the file and then overwrite it, even if we have less bytes of data than the initial data in the file.

Of course, depending on what you want to accomplish, you may not need the Seek and SetLength instructions, but if you are used to StreamReaders and StreamWriters working like the regular ones that create their FileStream implicitly, you may want to use these two instructions after the reading and before the writing of the file.

To see, exactly what these instructions do, use a file that contains a few lines of data, i.e. a few sentences. Run the program as I provide it (with these two instructions, Seek and SetLength, before writing to the file. See how the file ends up. Then remove either one or both of these instructions in order to see how the file ends up. And then you will understand what these two instructions do.

Now, to test the program for its safety in accessing a file concurrently, run the program not only once but many times. But before you do this, add one or more Console.ReadLine() and Console.WriteLine(“Something”) instructions inside the program, at places that you choose. The Console.ReadLine() instructions will make whichever is the current instance stop and wait for “Enter” to be pressed, thus prolonging the locking of the file and making the other instances wait. Each instance will unlock the file when the FileStream is closed. From the time the FileStream is opened to the time the FileStream is closed, only one instance can access the file, whether for reading, writing or both.

Advertisements

About Dimitrios Kalemis

I am a systems engineer specializing in Microsoft products and technologies. I am also an author. Please visit my blog to see the blog posts I have written, the books I have written and the applications I have created. I definitely recommend my blog posts under the category "Management", all my books and all my applications. I believe that you will find them interesting and useful. I am in the process of writing more blog posts and books, so please visit my blog from time to time to see what I come up with next. I am also active on other sites; links to those you can find in the "About me" page of my blog.
This entry was posted in Development. Bookmark the permalink.