4

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!

kofifus
  • 17,260
  • 17
  • 99
  • 173
  • Why you dont use the `lock`-statement? See https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/lock-statement – akop Dec 20 '18 at 08:45
  • 1
    Not completely sure if the using statement (so the try/finally block it makes) prevents this but I think the problem is the following: the thread that executes the code before the await (acquiring the lock) can be a different thread then the thread the executes the code after the await (releasing the lock). Since Monitor is re-entrant I think it tracks which thread owns the lock, I can imagine this error being thrown when another thread then the owning thread tries to release the Monitor. –  Dec 20 '18 at 08:45
  • knoop yes that is the problem .. how can I best solve it ? – kofifus Dec 20 '18 at 08:49
  • Ah sorry, thought you where unclear as to why this error occurred. As mentioned in the thread you linked semaphore seems to be the way to go: https://stackoverflow.com/questions/11001760/monitor-enter-and-monitor-exit-in-different-threads. –  Dec 20 '18 at 09:04

2 Answers2

6

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"
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 1
    ok does that mean SemaphoreSlim is not reentrant ? that will make it unusable for me ... – kofifus Dec 20 '18 at 09:13
  • 1
    @kofifus I *tried* to say "try `ReaderWriterLockSlim` - that has recursion support", but upon consideration that might be thread-bound too – Marc Gravell Dec 20 '18 at 09:15
  • ok so there is no asynchronous lock which is reenrtrant within the same thread and within all async operation from that thread ? – kofifus Dec 20 '18 at 09:17
  • 1
    @kofifus not AFAIK, no; you might need to engineer something using the async call-content, but... that's not fun; it would be easier to remove the need to be re-entrant – Marc Gravell Dec 20 '18 at 09:18
  • ok I have no idea how to engineer such a thing ... seems something handy .NET should have ... thanks for the input! – kofifus Dec 20 '18 at 09:28
  • upon further thinking I'm not sure such lock is even possible ... – kofifus Dec 20 '18 at 09:51
  • 1
    @kofifus Well if you think of the monitor as working with a key (in this case the key is always the id of the thread that owns the lock) there should be a possibility of creating something where you can pass a key around and if you call the lock with the matching key you'll be able to re-enter it. That way you don't rely on programmers having to check if they already own a lock but it would mean that you're passing around a key everywhere in your code that uses this locking. So it should certainly be possible, the question is whether it is desirable:-) –  Dec 20 '18 at 09:59
  • I guess I'm worried about deadlocks especially if mixing sync and async code .. not sure.. also see http://asherman.io/projects/csharp-async-reentrant-locks.html – kofifus Dec 20 '18 at 10:02
-2

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.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • 1
    In the context of `async`, this just isn't true or helpful; for example, your `SelectAsync` without an `await` (that you **cannot** use, for good reason): doesn't lock over the correct interval - it only holds the lock until the first incomplete awaitable operation. – Marc Gravell Dec 20 '18 at 09:07
  • 'Less code and simpler' yes in above cases but not when the code inside the using (or your 'with') is doing a lot of it's own access to the state .. you'll end up with a very deep lambda nesting ... – kofifus Dec 20 '18 at 09:45
  • @kofifus - This is the approach LINQ takes and that doesn't end up with deep lambda nesting. – Enigmativity Dec 20 '18 at 10:52
  • @MarcGravell - Sorry, I was overreaching there with the async code. I've removed it. – Enigmativity Dec 20 '18 at 10:54
  • I don't understand what Linq has to do with it .. with your approach you will have to be very careful inside your with or update that you're not using stale values which IMO is even more error prone than forgetting to lock .. anyways – kofifus Dec 20 '18 at 23:36
  • @kofifus - What stale values? – Enigmativity Dec 21 '18 at 03:52