29

I need to lock a section of code by string. Of course the following code is hideously unsafe:

lock("http://someurl")
{
    //bla
}

So I've been cooking up an alternative. I'm not normally one to post large bodies of code here, but when it comes to concurrent programming, I'm a little apprehensive about making my own synchronization scheme, so I'm submitting my code to ask if it's sane to do it in this way or whether there's a more straightforward approach.

public class StringLock
{
    private readonly Dictionary<string, LockObject> keyLocks = new Dictionary<string, LockObject>();
    private readonly object keyLocksLock = new object();

    public void LockOperation(string url, Action action)
    {
        LockObject obj;
        lock (keyLocksLock)
        {
            if (!keyLocks.TryGetValue(url,
                                      out obj))
            {
                keyLocks[url] = obj = new LockObject();
            }
            obj.Withdraw();
        }
        Monitor.Enter(obj);
        try
        {
            action();
        }
        finally
        {
            lock (keyLocksLock)
            {
                if (obj.Return())
                {
                    keyLocks.Remove(url);
                }
                Monitor.Exit(obj);
            }
        }
    }

    private class LockObject
    {
        private int leaseCount;

        public void Withdraw()
        {
            Interlocked.Increment(ref leaseCount);
        }

        public bool Return()
        {
            return Interlocked.Decrement(ref leaseCount) == 0;
        }
    }
}

I would use it like this:

StringLock.LockOperation("http://someurl",()=>{
    //bla
});

Good to go, or crash and burn?

EDIT

For posterity, here's my working code. Thanks for all the suggestions:

public class StringLock
{
    private readonly Dictionary<string, LockObject> keyLocks = new Dictionary<string, LockObject>();
    private readonly object keyLocksLock = new object();

    public IDisposable AcquireLock(string key)
    {
        LockObject obj;
        lock (keyLocksLock)
        {
            if (!keyLocks.TryGetValue(key,
                                      out obj))
            {
                keyLocks[key] = obj = new LockObject(key);
            }
            obj.Withdraw();
        }
        Monitor.Enter(obj);
        return new DisposableToken(this,
                                   obj);
    }

    private void ReturnLock(DisposableToken disposableLock)
    {
        var obj = disposableLock.LockObject;
        lock (keyLocksLock)
        {
            if (obj.Return())
            {
                keyLocks.Remove(obj.Key);
            }
            Monitor.Exit(obj);
        }
    }

    private class DisposableToken : IDisposable
    {
        private readonly LockObject lockObject;
        private readonly StringLock stringLock;
        private bool disposed;

        public DisposableToken(StringLock stringLock, LockObject lockObject)
        {
            this.stringLock = stringLock;
            this.lockObject = lockObject;
        }

        public LockObject LockObject
        {
            get
            {
                return lockObject;
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        ~DisposableToken()
        {
            Dispose(false);
        }

        private void Dispose(bool disposing)
        {
            if (disposing && !disposed)
            {
                stringLock.ReturnLock(this);
                disposed = true;
            }
        }
    }

    private class LockObject
    {
        private readonly string key;
        private int leaseCount;

        public LockObject(string key)
        {
            this.key = key;
        }

        public string Key
        {
            get
            {
                return key;
            }
        }

        public void Withdraw()
        {
            Interlocked.Increment(ref leaseCount);
        }

        public bool Return()
        {
            return Interlocked.Decrement(ref leaseCount) == 0;
        }
    }
}

Used as follows:

var stringLock=new StringLock();
//...
using(stringLock.AcquireLock(someKey))
{
    //bla
}
spender
  • 117,338
  • 33
  • 229
  • 351
  • 2
    It looks over-engineered to me, but difficult to say without knowing what problem it's supposed to solve. Is this just to avoid locking on a string? – LukeH Nov 19 '10 at 12:27
  • @LukeH, I'm trying to write an image cache where multiple clients (over a 100 or so) are likely to request the image simultaneously. The first hit will request the image from another server, and I'd like all other requests to the cache to block until this operation is complete so that they can retrieve the cached version. To my mind, this will require locking of this nature, but if there are any alternatives, I'm all ears. – spender Nov 19 '10 at 12:36
  • @Ani, yes I'm aware of this (as described in my post) and if I consider this approach to be a goer, I'll shore up the defences. – spender Nov 19 '10 at 12:37
  • By adding try catch I think is a good way. – Saeed Amiri Nov 19 '10 at 12:37
  • 1
    And you might consider making it not static. If it's static you can only use stringlocks for one purpose/module and it isn't reusable. – CodesInChaos Nov 19 '10 at 12:38
  • @CodeInChaos, the main point is, to be static, non static one is not good here. – Saeed Amiri Nov 19 '10 at 12:40
  • I'll probably go for a static instance rather than a static class. Makes it slightly more reusable. – spender Nov 19 '10 at 12:41
  • 1
    If the intention is to put this in a reusable library, it can't be static. If it *must* be `static`, then it must be also be `internal`. Otherwise, we've just moved the original interning problem elsewhere by creating our own, public pool that could unknowingly be shared by different modules. – Ani Nov 19 '10 at 12:42
  • @Saeed IMO each module which wants to use Stringlocks should declare its own instance of `StringLock` which it uses to lock. This might be in an instance or static variable, or even use dependency injection depending on how the module needs to use StringLocks. – CodesInChaos Nov 19 '10 at 12:44
  • I'm gonna go non-static so I can inject. @Ani: I also added a finally to make sure we clean up the adandoned mutex. – spender Nov 19 '10 at 12:50
  • 1
    Any reason not use `IDisposable` here instead of delegates? `using` blocks would be a nicer sugar to provide to the user. Additionally, it would be easier for the user to control the lifetime of the token that way. – Ani Nov 19 '10 at 12:53
  • This is certainly an interesting *idea*. – Ani Nov 19 '10 at 12:57
  • @CodInChaos, yes using a static variable, singleton instance are other ways, I'm saing the nature of usage is to use static manner, means lockable object should be present in heap not stack, like `readonly` objects. and @Ani, Where is a rule `static should be internal`? `Math` is static and is not internal. – Saeed Amiri Nov 19 '10 at 12:57
  • I don't think using with custom lock objects works correctly, in particular when the thread is aborted. For a standard `lock` the jitter can guarantee that the thread won't be aborted between entering the lock and entering the `try`...`finally`. On the other hand you should unload the appdomain in that case, so it might not matter. – CodesInChaos Nov 19 '10 at 12:58
  • @Saeed: That's not a *global* rule, but it does apply in this situation. Think about the consequences of 2 unrelated modules in the same app-domain choosing the same literal to lock on by accident. – Ani Nov 19 '10 at 12:59
  • @Saeed Math is immutable/has no state. And a singleton has the same faults as making the class static. A static variable declared by the module which uses the lock hasn't because locks aren't shared with unrelated modules then. IMO something that is static should not have mutable state except in very few select cases. – CodesInChaos Nov 19 '10 at 13:01
  • And be extremely careful if the same thread takes several locks at the same time. If thread 1 lock "A" and then "B" and thread 2 locks "B" and then "A" you have a potential deadlock. You might even want to code your StringLock class so it prevents taking two locks at the same time. – CodesInChaos Nov 19 '10 at 13:05
  • Do you *need* to remove the strings/locks from the dictionary when they're done with? If you're only dealing with a few thousand strings then I wouldn't bother and this then becomes possible in a couple of lines using a `ConcurrentDictionary`. If you do need to purge unused strings/locks then something like your current code is probably about as good as it's going to get (although I'd refactor it to use `IDisposable`/`using` as Ani suggests above). – LukeH Nov 19 '10 at 15:05
  • Your code seems to work correctly in the non reentrant case. But it's rather complicated, so I'm not sure about that. Consider using something simpler, like Ian Ringrose's solution. And I recommend adding a check against reentrancy. – CodesInChaos Nov 19 '10 at 15:07
  • 1
    As you are expecting so many requests do you need to make all the code async, so each blocking request does not use up a thread? **If so life gets a lot more complex!** – Ian Ringrose Nov 19 '10 at 17:04
  • Why do we need InterLocked increment decrement of leasecount, when you already access this code using a sync lock, lock (keyLocksLock)? –  Jun 03 '11 at 08:37
  • lock Statement (C# Reference) http://msdn.microsoft.com/en-us/library/c5kehkcz.aspx – Joel Purra Aug 15 '12 at 18:22
  • ConcurrentDictionary Class http://msdn.microsoft.com/en-us/library/dd287191.aspx – Joel Purra Aug 15 '12 at 18:48

9 Answers9

14

Locking by an arbitrary string instance would be a bad idea, because Monitor.Lock locks the instance. If you had two different string instances with the same content, that would be two independent locks, which you don't want. So you're right to be concerned about arbitrary strings.

However, .NET already has a built-in mechanism to return the "canonical instance" of a given string's content: String.Intern. If you pass it two different string instances with the same content, you will get back the same result instance both times.

lock (string.Intern(url)) {
    ...
}

This is simpler; there's less code for you to test, because you'd be relying on what's already in the Framework (which, presumably, already works).

Joe White
  • 94,807
  • 60
  • 220
  • 330
  • 1
    Monitor.Enter doesn't lock the instance. The instance is just a token used to implement the synchronization protocol. – Brian Rasmussen Nov 19 '10 at 13:38
  • This is *precisely* what the OP is trying to avoid. If another thread happened to be running code from a different module that decided to lock on the same interned string (by coincidence), there would be a completely unnecessary contention. The OP is trying to provide the user with the ease of string-locking, but without the .NET intern pool getting in the way. – Ani Nov 19 '10 at 14:30
  • 3
    In addition to [String.Intern() being slow](http://stackoverflow.com/a/19375402/709537), locking on interned strings risks performance issues and deadlocks, [even cross-AppDomains](http://stackoverflow.com/a/19375402/709537), ie apps running supposedly separated from your app. – Evgeniy Berezovsky Oct 15 '13 at 09:02
10

Another option is to get the HashCode of each URL, then divide it by a prime number and use it as an index into an array of locks. This will limit the number of locks you need while letting you control the probability of a “false locking” by choose the number of locks to use.

However the above is only worthwhile if it is too costly just have one lock per active url.

Ian Ringrose
  • 51,220
  • 55
  • 213
  • 317
  • 2
    The big advantage of that code is that it is quite simple to implement since unlike the Dictionary based code it doesn't need to remove lock objects to prevent a memoryleak. This is the only code in this topic I'd be confident to implement correctly without too much thought. And of course holding more than one lock at a time in a thread gets really ugly. – CodesInChaos Nov 19 '10 at 14:55
  • Thanks dude! I know this is like 4 years later but you just helped me come up with a great solution to a problem I'm having. :) – Haney Apr 22 '14 at 01:43
  • @Ian Ringrose can you please provide a code sample ? – sm_ Feb 10 '17 at 06:44
6

Here is a very simple, elegant and correct solution for .NET 4 using ConcurrentDictionary adapted from this question.

public static class StringLocker
{
    private static readonly ConcurrentDictionary<string, object> _locks = new ConcurrentDictionary<string, object>();

    public static void DoAction(string s, Action action)
    {
        lock(_locks.GetOrAdd(s, new object()))
        {
            action();
        }
    }
}

You can use this like so:

StringLocker.DoAction("http://someurl", () =>
{
    ...
});
Community
  • 1
  • 1
Zaid Masud
  • 13,225
  • 9
  • 67
  • 88
  • 3
    This implementation will never reclaim unused locks, and will end up consuming lots of memory in a long running process. – CaseyB Jul 21 '14 at 19:23
  • @CaseyB the lock will be reclaimed at the end of the `lock` code block. I think you mean that unused *lock objects* will stay in the `_locks` dictionary. This is true, but memory usage will be proportional to the number of URLs... and if you do some quick calculations you'll see that the number of URLs has to get **extremely** big for memory to be a problem. – Zaid Masud Jul 21 '14 at 22:53
  • @ZaidMasud: That's a valid idea, but maybe CaseyB has a point... it could be solved by wrapping `action();` inside a try-finally and removing the string entry from `_locks` in the finally clause – digEmAll Jun 08 '17 at 13:11
2

About 2 years ago i had to implement the same thing:

I'm trying to write an image cache where multiple clients (over a 100 or so) are likely to request the image simultaneously. The first hit will request the image from another server, and I'd like all other requests to the cache to block until this operation is complete so that they can retrieve the cached version.

I ended up with code doing pretty the same as yours (the dictionary of LockObjects). Well, yours seems to be better encapsulated.

So, I think you have quite a good solution. Just 2 comments:

  1. If you need even better peformance it maybe useful to utilize some kind of ReadWriteLock, since you have 100 readers and only 1 writer getting the image from another server.

  2. I am not sure what happens in case of thread switch just before Monitor.Enter(obj); in your LockOperation(). Say, the first thread wanting the image constructs a new lock and then thread switch just before it enters critical section. Then it could happen that the second thread enters the critical section before the first. Well could be that this is not a real problem.

STW
  • 44,917
  • 17
  • 105
  • 161
Adesit
  • 159
  • 5
  • That means my second point is relevant if you are relying on that the first thread entering critical section is the same which created the lock. For example if it mus be this 1st thread which triggers fetching the image from other server. – Adesit Nov 19 '10 at 13:55
2

You've created a pretty complex wrapper around a simple lock statement. Wouldn't it be better to create a dictionary of url's and create a lock object for each and everyone. You could simply do.

objLink = GetUrl("Url"); //Returns static reference to url/lock combination
lock(objLink.LockObject){
    //Code goes here
}

You could even simplify this by locking the objLink object directly wich could be the GetUrl("Url") string instance. (you'd have to lock the static list of strings though)

You're original code if very error prone. If the code:

if (obj.Return())
{
keyLocks.Remove(url);
}

If the original finally code throws an exception you're left with an invalid LockObject state.

CodingBarfield
  • 3,392
  • 2
  • 27
  • 54
  • The only exceptions this code should throw are async exceptions like StackOverflow, OutOfMemory and thread abortion in which case you should unload the appdomain anyways, so it wouldn't matter much. – CodesInChaos Nov 19 '10 at 16:38
0

I added a solution in Bardock.Utils package inspired by @eugene-beresovsky answer.

Usage:

private static LockeableObjectFactory<string> _lockeableStringFactory = 
    new LockeableObjectFactory<string>();

string key = ...;

lock (_lockeableStringFactory.Get(key))
{
    ...
}

Solution code:

namespace Bardock.Utils.Sync
{
    /// <summary>
    /// Creates objects based on instances of TSeed that can be used to acquire an exclusive lock.
    /// Instanciate one factory for every use case you might have.
    /// Inspired by Eugene Beresovsky's solution: https://stackoverflow.com/a/19375402
    /// </summary>
    /// <typeparam name="TSeed">Type of the object you want lock on</typeparam>
    public class LockeableObjectFactory<TSeed>
    {
        private readonly ConcurrentDictionary<TSeed, object> _lockeableObjects = new ConcurrentDictionary<TSeed, object>();

        /// <summary>
        /// Creates or uses an existing object instance by specified seed
        /// </summary>
        /// <param name="seed">
        /// The object used to generate a new lockeable object.
        /// The default EqualityComparer<TSeed> is used to determine if two seeds are equal. 
        /// The same object instance is returned for equal seeds, otherwise a new object is created.
        /// </param>
        public object Get(TSeed seed)
        {
            return _lockeableObjects.GetOrAdd(seed, valueFactory: x => new object());
        }
    }
}
Community
  • 1
  • 1
bardock
  • 1
  • 2
0

This is how I implemented this locking schema:

public class KeyLocker<TKey>
{
    private class KeyLock
    {
        public int Count;
    }

    private readonly Dictionary<TKey, KeyLock> keyLocks = new Dictionary<TKey, KeyLock>();

    public T ExecuteSynchronized<T>(TKey key, Func<TKey, T> function)
    {
        KeyLock keyLock =  null;
        try
        {              
            lock (keyLocks)
            {
                if (!keyLocks.TryGetValue(key, out keyLock))
                {
                    keyLock = new KeyLock();
                    keyLocks.Add(key, keyLock);
                }
                keyLock.Count++;
            }
            lock (keyLock)
            {
                return function(key);
            }
        }
        finally
        {         
            lock (keyLocks)
            {
                if (keyLock != null && --keyLock.Count == 0) keyLocks.Remove(key);
            }
        }
    }

    public void ExecuteSynchronized(TKey key, Action<TKey> action)
    {
        this.ExecuteSynchronized<bool>(key, k =>
        {
            action(k);
            return true;
        });
    }
}

And used like this:

private locker = new KeyLocker<string>();
......

void UseLocker()
{
     locker.ExecuteSynchronized("some string", () => DoSomething());
}
Jesús López
  • 8,338
  • 7
  • 40
  • 66
-1

Now I have the same issue. I decided to use simple solution: to avoid deadlocks create a new string with fixed unique prefix and use it as a lock object:

lock(string.Intern("uniqueprefix" + url))
{
//
}
  • 3
    That's slightly less terrible than not ensuring that it's interned, but still a very bad idea. You shouldn't be locking on data that is publicly available; you should lock on data that is only accessible to code blocks that ought to be able to lock on it. – Servy Oct 29 '13 at 17:41
-1

In most cases, when you think you need locks, you don't. Instead, try to use thread-safe data structure (e.g. Queue) which handles the locking for you.

For example, in python:

class RequestManager(Thread):
    def __init__(self):
        super().__init__()
        self.requests = Queue()
        self.cache = dict()
    def run(self):        
        while True:
            url, q = self.requests.get()
            if url not in self.cache:
                self.cache[url] = urlopen(url).read()[:100]
            q.put(self.cache[url])

    # get() runs on MyThread's thread, not RequestManager's
    def get(self, url):
        q = Queue(1)
        self.requests.put((url, q))
        return q.get()

class MyThread(Thread):
    def __init__(self):
        super().__init__()
    def run(self):
        while True:
            sleep(random())
            url = ['http://www.google.com/', 'http://www.yahoo.com', 'http://www.microsoft.com'][randrange(0, 3)]
            img = rm.get(url)
            print(img)
Lie Ryan
  • 62,238
  • 13
  • 100
  • 144