16

I have multiple asp.net web apps serving a set of files. Periodically, one will update the file before serving it, but it can't update the file if it is in use.

I could solve this issue by using a named mutex where the name is the file path (replacing the invalid characters of course). I've used this in other situations, but you can see how inefficient it is. Only one process will be able to serve the file at a time.

A reader/writer lock would be perfect, but they are designed to work within a single process. Plus I'd have to create a reader/writer lock for every file that might get updated, and there are a lot.

What I really need is a reader/writer lock that can be named like a mutex. Is there such a thing? Or can such a thing be created using the existing locks?

Charles
  • 2,642
  • 3
  • 33
  • 53
  • 1
    Is there anything wrong with simply attempting to open the file itself and then catching the appropiate exception to check whether it has been locked? Polling repeatedly shouldn't be a problem, if that's what's required. It seems like by far your easiest solution. – Noldorin Mar 12 '09 at 19:25
  • Is it possible to move to a database solution instead of using files? I only suggest this because what it sounds like you really want are transactions. – tvanfosson Mar 12 '09 at 19:33

4 Answers4

19

It's possible to simulate a reader/writer lock using a Mutex and a Semaphore. I wouldn't do it if I had to access it thousands of times per second, but for dozens or perhaps hundreds of times per second, it should work just fine.

This lock would allow exclusive access by 1 writer or concurrent access by N (possibly large, but you have to define it) readers.

Here's how it works. I'll use 10 readers as an example.

Initialize a named Mutex, initially unsignaled, and a named Semaphore with 10 slots:

  Mutex m = new Mutex(false, "MyMutex");
  Semaphore s = new Semaphore(10, 10, "MySemaphore");

Acquire reader lock:

// Lock access to the semaphore.
m.WaitOne();
// Wait for a semaphore slot.
s.WaitOne();
// Release mutex so others can access the semaphore.
m.ReleaseMutex();

Release reader lock:

s.Release();

Acquire writer lock:

// Lock access to the seamphore
m.WaitOne();
// Here we're waiting for the semaphore to get full,
// meaning that there aren't any more readers accessing.
// The only way to get the count is to call Release.
// So we wait, then immediately release.
// Release returns the previous count.
// Since we know that access to the semaphore is locked
// (i.e. nobody can get a slot), we know that when count
// goes to 9 (one less than the total possible), all the readers
// are done.
s.WaitOne();
int count = s.Release();
while (count != 9)
{
    // sleep briefly so other processes get a chance.
    // You might want to tweak this value.  Sleep(1) might be okay.
    Thread.Sleep(10);
    s.WaitOne();
    count = s.Release();
}

// At this point, there are no more readers.

Release writer lock:

m.ReleaseMutex();

Although fragile (every process using this better have the same number for the semaphore count!), I think it will do what you want as long as you don't try to hit it too hard.

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • I don't quite understand. If you create a mutex and semaphore in one of the apps, how is that accessible to the other apps? – Trevor May 08 '16 at 06:56
  • 2
    @Trevor: Because they're operating system objects. Named objects are globally accessible, based on access rights. See https://msdn.microsoft.com/en-us/library/f55ddskf(v=vs.110).aspx. Basically, Windows objects that are named are globally accessible, across processes. – Jim Mischel May 09 '16 at 04:34
  • @JimMischel: What is the recommended timespan to release the writer mutex? – ivorykoder Apr 14 '17 at 10:07
  • 1
    @ivorykoder: The general idea is that you hold any lock for as brief a time as possible. That's especially true in the case of the writer lock here, as that's an exclusive lock: no other thread or process can obtain a read or write lock if you're holding the write lock. So, acquire the writer lock, write the data you need to write, and then release. The recommended timespan is, "as long as you need it, but no longer." – Jim Mischel Apr 14 '17 at 10:52
2

I appreciate Jim Mischel's fine answer, but I see opportunity for performance improvement by avoiding Thread.Sleep() and avoiding lock contention when multiple readers try to acquire at the same time!

Initialization

  Mutex writer = new Mutex(false, "Global\\MyWriterMutex");
  Semaphore readers = new Semaphore(int.MaxValue, int.MaxValue, "Global\\MyReadersSemaphore");
  EventWaitHandle readAllowed = new EventWaitHandle(true, EventResetMode.ManualReset, "Global\\MyReadAllowedEvent");
  EventWaitHandle readFinished = new EventWaitHandle(false, EventResetMode.ManualReset, "Global\\MyReadFinishedEvent");

Reader

  while (true)
  {
    // signal that I'm reading 
    readers.WaitOne();

    // check whether I'm actually allowed to read
    if (readAllowed.WaitOne(0))
    {
      break; // great!
    }

    // oops, nevermind, signal that I'm not reading
    readers.Release();
    readFinished.Set();

    // block until it's ok to read
    readAllowed.WaitOne();
  }

  try
  {
    readData();
  }
  finally
  {
    // signal that I'm no longer reading
    readers.Release();
    readFinished.Set();
  }

Writer

  // block until I am the only writer
  try
  {
    writer.WaitOne();
  }
  catch (AbandonedMutexException)
  {
    // The mutex was abandoned in another process, but it was still acquired
  }

  // signal that readers need to cease
  readAllowed.Reset();

  // loop until there are no readers
  int readerCount = -1;
  while (readerCount != 0)
  {
    // wipe the knowledge that a reader recently finished
    readFinished.Reset();

    // check if there is a reader
    readers.WaitOne();
    readerCount = int.MaxValue - (readers.Release() + 1);
    if (readerCount > 0)
    {
      // block until some reader finishes
      readFinished.WaitOne();
    }
  }

  try
  {
    writeData();
  }
  finally
  {
    // signal that readers may continue, and I am no longer the writer
    readAllowed.Set();
    writer.ReleaseMutex();
  }
1

How about this? Don't serve up files. Serve up copies of files. When you need to make a change, create a new file, and serve up a copy of that from then on.

Matt Brunell
  • 10,141
  • 3
  • 34
  • 46
0

I don't think there's anything that meets your needs as stated (although I reserve the right to be wrong).

You could serve the file using a service. This solves two problems. First, as you stated, the problem of concurrency. Also, even if you can achieve synchronization, it becomes more difficult and ugly if you start load-balancing. Serving the file using a service will likely cost you in performance, but solves your synchronization problem.

Michael Meadows
  • 27,796
  • 4
  • 47
  • 63