Composable Locks using LINQ. Can anyone see any problem with this?

355 views Asked by At

I was playing around with LINQ and I came up with the following idea for composing locks taking advantage of C# Monadic syntax. It seems too simple, so I thought let me post it on StackOverflow and see if anyone can quickly spot any major problems with this approach.

If you comment the lock inside the atomic implementation you'll see the accounts getting corrupted.

This idea is about being able to compose operations on the accounts without explicit locking.

using System;
using System.Collections.Generic;
using System.Linq;

namespace AtomicLinq
{
    public static class AccountCombinators
    {
        public static IAtomic <Unit> TransferAndReverse(this Account accA, 
                                                      Account accB, 
                                                      double amount)
        {
            return from a in accA.TransferTo(accB, amount)
                   from b in accB.TransferTo(accA, amount)
                   select new Unit();
        }

        public static IAtomic<Unit> TransferTo(this Account accA, 
                                              Account accB, 
                                              double amount)
        {
            return from a in accA.Withdraw(amount)
                   from b in accB.Deposit(amount)
                   select new Unit();
        }

        public static IAtomic<double> Withdraw(this Account acc, double amount)
        {
            return Atomic.Create(() => acc.Amount -= amount);
        }

        public static IAtomic<double> Deposit(this Account acc, double amount)
        {
            return Atomic.Create(() => acc.Amount += amount);
        }
    }

    static class Program
    {
        static void Main(string[] args)
        {
            var accA = new Account("John") { Amount = 100.0 };
            var accB = new Account("Mark") { Amount = 200.0 };

            var syncObject = new object();
            Enumerable.Range(1, 100000).AsParallel().Select(_ => accA.TransferAndReverse(accB, 100).Execute(syncObject)).Run();

            Console.WriteLine("{0} {1}", accA, accA.Amount);
            Console.WriteLine("{0} {1}", accB, accB.Amount);

            Console.ReadLine();
        }        
    }    

    public class Account
    {
        public double Amount { get; set; }
        private readonly string _name;

        public Account(string name)
        {
            _name = name;
        }

        public override string ToString()
        {
            return _name;
        }
    }

    #region Atomic Implementation

    public interface IAtomic<T>
    {
        T Execute(object sync);
    }

    public static class Atomic
    {
        public static IAtomic<T> Create<T>(Func<object, T> f)
        {
            return new AnonymousAtomic<T>(f);
        }

        public static IAtomic<T> Create<T>(Func<T> f)
        {
            return Create(_ => f());
        }

        public static IAtomic<T> Aggregate<T>(this IEnumerable<IAtomic<T>> xs)
        {
            return xs.Aggregate((x, y) => from a in x
                                          from b in y
                                          select b);
        }

        public static IAtomic<K> SelectMany<T, V, K>(this IAtomic<T> m, Func<T, IAtomic<V>> f, Func<T, V, K> p)
        {
            return Create(sync =>
                              {
                                  var t = m.Execute(sync);
                                  var x = f(t);
                                  return p(t, x.Execute(sync));
                              });
        }
    }

    public class AnonymousAtomic<T> : IAtomic<T>
    {
        private readonly Func<object, T> _func;

        public AnonymousAtomic(Func<object, T> func)
        {
            _func = func;
        }

        public T Execute(object sync)
        {
            lock (sync)  // Try to comment this lock you'll see that the accounts get corrupted
                return _func(sync);
        }
    }

    #endregion Atomic Implementation   
}



1

There are 1 answers

1
gjvdkamp On

A couple of simple points from me:

1) It doesn't compile, am I missing something? Specific C# preview or something?

2) You're using one syncObject that will become a major bottleneck. Essentially you're turning the whole thing back into a single threaded application. There are easier ways of doing that. This automagical concurrency comes at considerable performance cost.

3) There's only one field here that can be corrupted, why not lock on its Accessors? You could still compose operations on them. True, the class designer would have to take note of locking and concurrency. But on the other hand, using your approach the programmer would have to understand your composing classes. I'd go with normal locking because most people know it already.

It's an interesting thought, but I don't see the upside of doing this this way yet and there are some drawbacks. But there's a lot of thinking about transactional memory etc. This could lead to something.

GJ