Using a Monitor lock in async or sync operations

Viewed 297

I am trying to come with a solution for locking a global resources in a way that mandate locking for access and that is usable for sync or async operations.

I created a general class State:

public class State<T>
{
  public class LockedState : IDisposable {
    public LockedState(State<T> state) { this.state = state; Monitor.Enter(this.state.theLock); } // would be great if this could be private
    public void Dispose() => Monitor.Exit(this.state.theLock);
    public ref T Val { get { return ref state.t; } }

    readonly State<T> state;
  };

  public State() { }
  public State(T t) { this.t = t; }
  public LockedState Locked { get { return new LockedState(this); } }
  readonly object theLock = new object();
  T t;
}

The idea here is that I can have a global 'store' ie in the Program class:

public class Program {
    static public readonly State<ImmutableList<int>> TheList = new State<ImmutableList<int>>(ImmutableList<int>.Empty);
    static public readonly State<SomeType> SomeType = new State<SomeType>(SomeType());
}

and then the only way to access the state is by acquiring the lock like this:

using (var lTheList = Program.TheList.Locked) {
   lTheList.Val = lTheList.Val.Add(5));
}

which in my case works much better than a normal lock as it forces locking before get/set (you cannot forget to lock)

(side note is this a good strategy ?)

The issue is I cannot use the above in async code:

using (var someType = Program.SomeType.Lock()) {
   var x = await someType.Val.SomeAsyncOp();
}

I get an exception: Object synchronization method was called from an unsynchronized block of code

I found this post How to protect resources that may be used in a multi-threaded or async environment? which has an AsyncLock class However I couldn't understand how to fit a class like AsyncLock into my StateLock class ..

Can State.Lock() return a locked state that is usable for both sync and async callers ? that is really what I am looking for !

If not what is my best way forward ? using SemaphoreSlim and having a State.Lock() and a State.AsyncLock() ?

Thanks!

2 Answers

You can't use Monitor here, as Monitor (aka lock) is inherently thread-based and requires you to "exit" the lock from the same thread that acquired the lock - which is exactly the situation you rarely expect in async code. Instead, consider a SemaphoreSlim, from which you can take and release a token, but which is not thread-bound.

Pseudo-code:

// usually a field somewhere
SemaphoreSlim obj = new SemaphoreSlim(1);

// acquire lock:
obj.Wait(); // or await obj.WaitAsync() if want async acquire

// release lock:
obj.Release(); // often in a "finally"

You code is a worry because any malicious programmer can take a lock on Program.TheList.Lock() without calling .Dispose() and thus causing any future callers from getting stuck inside the constructor. Constructors should always execute fast and cleanly to avoid corruption to the run-time.

Your code is all kinds of code smell.

It's better to pass lambdas into your code and lock around that.

Try this code:

public class State<T>
{
    public State() { }
    public State(T t) { this.t = t; }

    private T t = default(T);
    private readonly object theLock = new object();

    public void With(Action<T> action)
    {
        lock (this.theLock)
        {
            action(this.t);
        }
    }

    public void Update(Func<T, T> action)
    {
        lock (this.theLock)
        {
            this.t = action(this.t);
        }
    }

    public R Select<R>(Func<T, R> action)
    {
        lock (this.theLock)
        {
            return action(this.t);
        }
    }
}

Now, along with your Program class, you can do this:

Program.TheList.Update(t => t.Add(5));

Less code and simpler.

You can also do this:

async void Main()
{
    var wc = new State<WebClient>(new WebClient());

    string source = wc.Select<string>(x => x.DownloadString(new Uri("http://www.microsoft.com")));

    Console.WriteLine(source);
}

I hope this helps.

Related