3

I have a process that multiple users may call. It's a very expensive query, but it should only ever need to be run every 5 minutes or so to refresh some archive data. I have a lock right now so that we don't run the process several times at once, which would bring the system to it's knees, but each user then has to wait for the previous locks to run before they can run. If there are 3-4 users waiting for the lock, the 4th user has to wait upwards of 20 minutes for their query to run.

What I would like to do is to lock this object and run the query on the first request. If any other requests come in, have them wait for the current lock to finish, then return without actually executing the query.

Is there anything built into .Net that can accomplish this, or do I need to write some specific code for this lock?

Scottie
  • 11,050
  • 19
  • 68
  • 109
  • 1
    This sounds like a problem that needs more than this for a solution. I'd probably start thinking about some sort of sane caching strategy. – J... Jun 17 '13 at 19:03
  • I did not downvote, but maybe the one who did was expecting you to know about mutex features of .net and have come back with some code already from [this question](http://stackoverflow.com/questions/229565/what-is-a-good-pattern-for-using-a-global-mutex-in-c) – explunit Jun 17 '13 at 19:21
  • @Scottie - no downvote from me, but I expect because the question is perhaps a bit broad and lacks implementation details about your present solution. – J... Jun 17 '13 at 20:03

2 Answers2

4

You can do this with a ManualResetEvent and a lock.

private object _dataLock = new object();
private ManualResetEvent _dataEvent = new ManualResetEvent(false);

private ArchiveData GetData()
{
    if (Monitor.TryEnter(_dataLock))
    {
        _dataEvent.Reset();  // makes other threads wait on the data

        // perform the query

        // then set event to say that data is available
        _dataEvent.Set();
        try
        {
            return data;
        }
        finally
        {
            Monitor.Exit(_dataLock);
        }
    }

    // Other threads wait on the event before returning data.
    _dataEvent.WaitOne();
    return data;
}

So the first thread to get there obtains the lock and clears the _dataEvent, indicating that other threads will have to wait for the data. There's a race condition here in that if the second client gets there before _dataEvent is reset, it will return old data. I view that as acceptable, considering that it's archive data and the window of opportunity for that to happen is pretty small.

Other threads that come through try to obtain the lock, fail, and get blocked by the WaitOne.

When the data is available, the thread that did the query sets the event, releases the lock, and returns the data.

Note that I didn't put the entire lock body in a try...finally. See Eric Lippert's Locks and Exceptions do not mix for the reasons why.

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
2

This solution is for those of you who cannot accept the possibility of more than one caller executing the "preparation code".

This technique avoids using locks for the "normal" use-case scenario for when the data has been prepared. Locking does have some overhead. Which might or might not apply to your use-case.

The pattern is called an if-lock-if pattern, IIRC. I have tried to annotate inline to the best of my abilities:

bool dataReady;
string data;
object lock = new object();

void GetData()
{
    // The first if-check will only allow a few through. 
    // Normally maybe only one, but when there's a race condition 
    // there might be more of them that enters the if-block. 
    // After the data is ready though the callers will never go into the block, 
    // thus avoids the 'expensive' lock.
    if (!dataReady)
    {
        // The first callers that all detected that there where no data now
        // competes for the lock. But, only one can take it. The other ones
        // will have to wait before they can enter. 
        Monitor.Enter(lock);
        try
        {
            // We know that only one caller at the time is running this code
            // but, since all of the callers waiting for the lock eventually
            // will get here, we have to check if the data is still not ready.
            // The data could have been prepared by the previous caller,
            // making it unnecessary for the next callers to enter.
            if (!dataReady)
            { 
                // The first caller that gets through can now prepare and 
                // get the data, so that it is available for all callers.
                // Only the first caller that gets the lock will execute this code.
                data = "Example data";

                // Since the data has now been prepared we must tell any other callers that
                // the data is ready. We do this by setting the 
                // dataReady flag to true.
                Console.WriteLine("Data prepared!");
                dataReady = true;
            }
        }
        finally
        {
            // This is done in try/finally to ensure that an equal amount of 
            // Monitor.Exit() and Monitor.Enter() calls are executed. 
            // Which is important - to avoid any callers being left outside the lock. 
            Monitor.Exit(lock);
        }
    }

    // This is the part of the code that eventually all callers will execute,
    // as soon as the first caller into the lock has prepared the data for the others.
    Console.WriteLine("Data is: '{0}'", data);
}

MSDN reference:

Spiralis
  • 3,232
  • 2
  • 39
  • 53