0
\$\begingroup\$

I've been working for a while on locking resources (state objects) so that the only way to access them is by acquiring a lock. This moves the responsibility for locking from the user of the state to the state itself. I wanted something with a lot of syntactic sugar so that once the lock is acquired the state object is used via a reference without a need for special getters and setters.

namespace Lockable {
  public delegate void ActionRef<T>(ref T r1);
  public delegate void ActionIn<T>(in T r1);
  public delegate RES FuncRef<T, RES>(ref T r1);
  public delegate RES FuncIn<T, RES>(in T r1);
  public delegate void ActionRef<T1, T2>(ref T1 r1, ref T2 r2);
  public delegate void ActionIn<T1, T2>(in T1 r1, in T2 r2);
  public delegate RES FuncRef<T1, T2, RES>(ref T1 r1, ref T2 r2);
  public delegate RES FuncIn<T1, T2, RES>(in T1 r1, in T2 r2);
  public delegate void ActionRef<T1, T2, T3>(ref T1 r1, ref T2 r2, ref T3 r3);
  public delegate void ActionIn<T1, T2, T3>(in T1 r1, in T2 r2, in T3 r3);
  public delegate RES FuncRef<T1, T2, T3, RES>(ref T1 r1, ref T2 r2, ref T3 r3);
  public delegate RES FuncIn<T1, T2, T3, RES>(in T1 r1, in T2 r2, in T3 r3);

  public class Lockable<T> where T : new() {
    public Lockable() => this.val = new T();
    public Lockable(T val) => this.val = val;
    readonly object theLock = new object();
    T val;

    public void Lock(ActionRef<T> f) { lock (theLock) f(ref val); }
    public void Lock(ActionIn<T> f) { lock (theLock) f(in val); }
    public TRES Lock<TRES>(FuncRef<T, TRES> f) { lock (theLock) return f(ref val); }
    public TRES Lock<TRES>(FuncIn<T, TRES> f) { lock (theLock) return f(in val); }

    public class TwoLockable<T2> where T2 : new() {
      public TwoLockable(Lockable<T> val1, Lockable<T2> val2) {
        this.l1 = val1;
        this.l2 = val2;
      }
      readonly Lockable<T> l1;
      readonly Lockable<T2> l2;

      public void Lock(ActionRef<T, T2> f) { lock (l1.theLock) lock (l2.theLock) f(ref l1.val, ref l2.val); }
      public void Lock(ActionIn<T, T2> f) { lock (l1.theLock) lock (l2.theLock) f(in l1.val, in l2.val); }
      public TRES Lock<TRES>(FuncRef<T, T2, TRES> f) { lock (l1.theLock) lock (l2.theLock) return f(ref l1.val, ref l2.val); }
      public TRES Lock<TRES>(FuncIn<T, T2, TRES> f) { lock (l1.theLock) lock (l2.theLock) return f(in l1.val, in l2.val); }

      public class ThreeLockable<T3> where T3 : new() {
        public ThreeLockable(Lockable<T> val1, Lockable<T2> val2, Lockable<T3> val3) {
          this.l1 = val1;
          this.l2 = val2;
          this.l3 = val3;
        }
        readonly Lockable<T> l1;
        readonly Lockable<T2> l2;
        readonly Lockable<T3> l3;

        public void Lock(ActionRef<T, T2, T3> f) { lock (l1.theLock) lock (l2.theLock) lock (l3.theLock) f(ref l1.val, ref l2.val, ref l3.val); }
        public void Lock(ActionIn<T, T2, T3> f) { lock (l1.theLock) lock (l2.theLock) lock (l3.theLock) f(in l1.val, in l2.val, in l3.val); }
        public TRES Lock<TRES>(FuncRef<T, T2, T3, TRES> f) { lock (l1.theLock) lock (l2.theLock) lock (l3.theLock) return f(ref l1.val, ref l2.val, ref l3.val); }
        public TRES Lock<TRES>(FuncIn<T, T2, T3, TRES> f) { lock (l1.theLock) lock (l2.theLock) lock (l3.theLock) return f(in l1.val, in l2.val, in l3.val); }
      }
      public ThreeLockable<T3> Combine<T3>(Lockable<T3> l3) where T3 : new() => new ThreeLockable<T3>(this.l1, this.l2, l3);
    }
    public TwoLockable<T2> Combine<T2>(Lockable<T2> l2) where T2 : new() => new TwoLockable<T2>(this, l2);
  }
}

With that code, I can now do this:

public readonly Lockable<Customers> Customers = new Lockable<Customers>();

and now the only way to access Customers is via the Lock method like this:

Customers.Lock((ref Customers Customers) => {
    Log(Customers.GetPhone("John"));
    Customers=new Customers();
    Customers.Add("Mary");
});

or to return a value I can do:

var count = Customers.Lock((ref Customers Customers) => Customers.Count());

Note that you can use the above to modify the state without locking i.e.:

var UnlockedCustomers = Customers.Lock((ref Customers Customers) => Customers);
UnlockedCustomers.Add("Dave"); // unlocked !!!

This is by design as I wanted to allow that but make it very explicit in the code (unlike forgetting to lock..), if this is not desired the locks returning values need to be removed (FuncRefs etc.).

If I need to lock both I combine the Locks:

public readonly Lockable<Agents> Agents = new Lockable<Agents>();

Customers.Combine(Agents).Lock((ref Customers Customers, ref Agents Agents) => {
  // both Agents and Customers are locked here !
});

I included versions of combining 2 and 3 Lockables. More can be added. Note that as with locks you have to always combine in the same order or risk deadlocks. It would be interesting to think of ways around this.

\$\endgroup\$
  • \$\begingroup\$ Why does Lock have various ref and in overloads, but no 'plain' variants? \$\endgroup\$ – Pieter Witvoet Jan 9 at 10:14
  • \$\begingroup\$ Without a ref the lambdas will get their own local reference to the state (val) and assigning to it will only change the local copy which is confusing and error prone. I mostly use this with immutable types so being able to reassign was a main goal .. (think of a redux like 'store') \$\endgroup\$ – kofifus Jan 9 at 10:19
  • 1
    \$\begingroup\$ I'm not sure this pattern improves readability of the code significantly. In my opinion you might be adding quite a bit of complexity for other programmers to maintain this. \$\endgroup\$ – Svek Jan 9 at 22:35
  • \$\begingroup\$ svek the idea is not to improve readability but to prevent access without locking. But still, the Lockable class itself is complex but using it is (to me) very straight forward \$\endgroup\$ – kofifus Jan 9 at 22:40
2
\$\begingroup\$

The only thing I can spot here is in this line

 public class Lockable<T> where T : new()

The constraint is probably not bulletproof. What would happen if I did something like

 var foo = Lockable<Lockable<Foo>>();

I would imagine this would make for an exciting explosion.

\$\endgroup\$
  • 2
    \$\begingroup\$ :) yeah probably .. too scared to try \$\endgroup\$ – kofifus Jan 9 at 22:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.