Search code examples
c#multithreadingmanualresetevent

ManualResetEventSlim and Lock


I have a piece of data that takes quite a lot of time to fetch. I have different ways of figuring out if new data should be fetched or if I can use my current "cache" theResult When someone asks for that piece of data I want to be able to both do a blocking and non blocking return.

Im not sure what the best way is to do that, I was considering something with ManualResetEventSlim and a lock:

NonBlocking:

theState = State.None;

public Data GetDataNonBlocking(){

   lock(_myLock){
        if (theState == State.Getting)
          return null;
        if (theState == State.Complete
          return theData;

        theState = State.Getting;
        _resetEvent.Reset();
        Task.Factory.StartNew(
           ()=>{                     
                 //<...Getting data.....>
                 theData= ...data....;
                 lock(_myLock){
                    theState = State.Complete;
                   _resetevent.Set();  
                 }
                });
         return null;
   }
}

Blocking:

public Data GetDataBlocking(){

  lock(_myLock){
       if (theState == State.Getting){
           _resetevent.Wait();
           return theData;
       }
       if (theState == State.Complete)
          return theData;

       _resetevent.Reset();
       theState = State.Getting;
  }
  //.....
  theData= 1234;
  lock(_myLock){
     State = State.Complete;
     _resetevent.Set();
  }
  return theData;
}

But I'm not certain that is the way to do a thing like that. For example the _resetEvent.Wait() inside a lock(...){}?


Solution

  • I think that your encapsulation could use a little tweaking. For example, I think that you should separate the code that gets the data asynchronously to just this:

    static class DataFactory
    {
        internal static DataType GetData()
        {
            // Return the data.
            return new DataType();
        }    
    }
    

    Then, your class instance can worry separately about the state, and use the Task<T> to facilitate that:

    class DataManager
    {
        // The lock on the operation.
        private readonly object lockObj = new object();
    
        // The state.
        private State theState = State.None;
    
        // The task to get the state.
        private Task<DataType> getDataTask;
    
        public DataType GetDataAsync()
        {        
           lock(lockObj)
           {
               if (theState == State.Getting)
                   return null;
               if (theState == State.Complete
                   return getDataTask.Result;
    
               // Set the state to getting.
               theState = State.Getting;
    
               // Start the task.
               getDataTask = Task.Factory.StartNew(() => {                     
                   // Get the data.
                   DataType result = DataFactory.GetData();
    
                   // Lock and set the state.
                   lock (lockObj)
                   {
                       // Set the state.
                       theState = State.Complete;
                   }
    
                   // Return the result.
                   return result;
               });
    
               // Return null to indicate the operation started
               // (is in "getting" state).
               return null;
           }
        }
    }
    

    Now, because you are using Task<T>, your GetDataBlocking (I think it should be called GetData) method becomes very simple:

    public DataType GetData()
    {
        // Get the data async, if the result is non null, then
        // return it.
        DataType result = GetDataAsync();
    
        // If it is non-null, return it.
        if (result != null) return result;
    
        // If at this point, the operation has been kicked off
        // to load the data.  Just wait on the task and return the result then.
        getDataTask.Wait();
    
        // Return the async data again, it will just return the
        // result from the task.
        return GetDataAsync();
    }
    

    In the end, I think you should keep more in line with the traditional async patterns exposed in .NET (either the Begin/End pattern, or event-based), as they will allow you to plug into other pipelines more easily.