Frage

I am a bit new in paralell programming and trying to find a solution for a next problem: Lets have a simple function:

private void doRefreshData()
{
  items = getUpdatedData();
}

getUpdatedData() is a really time comsumpting function and return dictionary. Problem is, that function doRefreshData() may and will be called from multiple threads at once. I would like to achieve, that only first thread will run function, others just wait for the result. Simple lock() is not good, because other threads will also run getUpdatedData(), even if it is not necessary - they have a work already done by other thread.

So I need something like:

private void doRefreshData()
{
  if (isAlreadyRunning) {
    waitForResult();
    return;
  }
}

  items = getUpdatedData();
}

I know, there is a workaround, but I am sure, that there is already a solution for this problem (Mutex?,Auto/ManualResetEvent?, SomethingElseWithFancyName?)

Update: After the first thread finished work and others also left, it is necessary to allow new thread to run this function - it is OK to ask for a new, refreshed data, even a few seconds later.

War es hilfreich?

Lösung

You could consider using a variant of the code that the lock statement actually generates by using Monitor.TryEnter() instead of Monitor.Enter:

if (Monitor.TryEnter(_myLock))
{
    try
    {
        // Your code
    }
    finally
    {
        Monitor.Exit(_myLock);
    }
}
else
{
    // Do something else
}

Then if the lock has already been obtained by an other thread, you can do something else instead.

Andere Tipps

You can easily implement the required logic using tasks and async/await.

The doRefreshData method should return a Task<TResult> of the appropriate type. Here's a dummy method that does nothing for two seconds and then returns a "result":

async Task<object> RealGetData()
{
    await Task.Delay(2000);
    return 42;
}

Since you want multiple methods to request the data concurrently and have their requests satisfied with the same result, you need to somehow remember that there is a "get data" operation already underway. You also need some kind of thread synchronization to eliminate race conditions, so:

private Task<object> currentCalculation;
private object lockTarget = new object(); // just for lock()

Now it's trivial to write an async method that either starts a new calculation if none is pending, or else hooks you up to receive the result of the pending one:

async Task<object> GetData()
{
    lock (lockTarget)
    {
        if (currentCalculation != null && !currentCalculation.IsCompleted)
        {
            return currentCalculation;
        }

        return currentCalculation = Task.Run<object>(RealGetData);
    }
}

Using this is super easy: any time you want access to "fresh" data, write await GetData() and you will always get a new result back; the code will automatically queue you up to get the result being currently calculated or start a new calculation for you. For example, this will start off a single calculation and satisfy all 10 requests with its result:

for (var i = 0; i < 10; ++i) {
    Console.WriteLine(await GetData());
}

If it only needed to fire once, I'd suggest using Lazy<T>. As it is, I'd use Task<T> and some Interlocked calls. Something like:

private Task<DataValue> _pendingRefresh;

private void doRefreshData()
{
    var tcs = new TaskCompletionSource<DataValue>();
    //See if there's one in flight
    var task = Interlocked.CompareExchange(ref _pendingRefresh, tcs.Task, null);
    if (task != null)
    {
        task.Wait(); //Or async, etc
        var items = task.Result;
    }
    else
    {
      try{
        var items = getUpdatedData();
        tcs.SetResult(items); //?
      } catch (Exception ex) {
          tcs.SetException(ex);
          throw;
      } finally {
        //Allow a new call to run 
        Interlocked.Exchange(ref _pendingRefresh, null); 
      }
    }
}

Basically, only one caller will ever be able to change _pendingRefresh from being null to not null. It will then do the work, and signal the Task once the work is complete. Other callers go into the top half of the if and wait for the Task to be marked as complete.

Once the "caller that was allowed to do the work" has finished and signalled the task, it sets _pendingRefresh back to null. To allow a new caller to do the work and make a new call.


Note, I've also added some better error handling so that hopefully, if getUpdatedData throws an exception, you don't end up with lots of callers stuck waiting for a task to complete that never will. They'll get errors, but at least you'll know about it on all affected threads.

My advice would be to use a named Mutex here. The reason to go for named here is so that it is easier to set a shared synchronisation point. Not naming a mutex only works when you share the mutex across threads. You could do that here, but I found that naming them is easier.

// Create a new named mutex and try to obtain a lock on it immediately.
// The WaitOne operation below ensures that you will have to wait when
// another thread has the lock.
Mutex mutex = new Mutex(true,"SynchronisationPoint");

// Wait for the mutex to become available.
// If you get beyond this point it means that you are the only 
// one performing this operation.
mutex.WaitOne();

// Check if the operation was already completed.
// When it was, do nothing here.
if(isCompleted) {
  return;
}

// Perform the operation.
items = getUpdatedData();
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top