按字符串锁定。这样安全/理智吗?

时间:2010-11-19 12:20:39

标签: c# multithreading locking

我需要按字符串锁定一段代码。当然,以下代码非常不安全:

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

所以我一直在做一个替代方案。我通常不会在这里发布大量的代码,但是当涉及到并发编程时,我有点担心我自己的同步方案,所以我提交我的代码来询问是否理智这样做以这种方式或是否有一个更直接的方法。

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;
        }
    }
}

我会像这样使用它:

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

好转,还是崩溃和烧伤?

修改

对于后代,这是我的工作代码。感谢您的所有建议:

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;
        }
    }
}

使用如下:

var stringLock=new StringLock();
//...
using(stringLock.AcquireLock(someKey))
{
    //bla
}

9 个答案:

答案 0 :(得分:14)

锁定任意字符串实例是个坏主意,因为Monitor.Lock会锁定实例。如果您有两个具有相同内容的不同字符串实例,那么这将是两个您不想要的独立锁。所以你关心任意字符串是正确的。

但是,.NET已经有一个内置机制来返回给定字符串内容的“规范实例”:String.Intern。如果您传递两个具有相同内容的不同字符串实例,则两次都会返回相同的结果实例。

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

这更简单;您可以使用较少的代码进行测试,因为您将依赖于框架中已有的内容(可能已经有效)。

答案 1 :(得分:10)

另一种选择是获取每个URL的HashCode,然后将其除以素数,并将其用作锁数组的索引。这将限制您需要的锁定数量,同时通过选择要使用的锁定数量来控制“错误锁定”的可能性。

然而,如果成本过高,每个活动网址只有一个锁,那么上述情况是值得的。

答案 2 :(得分:5)

这是一个非常简单,优雅而正确的.NET 4解决方案,使用ConcurrentDictionary改编自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();
        }
    }
}

您可以这样使用:

StringLocker.DoAction("http://someurl", () =>
{
    ...
});

答案 3 :(得分:2)

大约2年前,我必须实施同样的事情:

  

我正在尝试编写图像缓存   多个客户(超过100或   所以)可能会要求提供图像   同时。第一次打击会   请求来自其他服务器的图像,   我想要所有其他要求   缓存以阻止此操作   完成,以便他们可以检索   缓存版本。

我最终得到的代码和你的代码一样(LockObjects的字典)。好吧,你的封装似乎更好。

所以,我认为你有一个很好的解决方案。只有2条评论:

  1. 如果您需要更好的性能,使用某种ReadWriteLock可能很有用,因为您有100个读者,只有1个编写者从另一个服务器获取图像。

  2. 我不确定在线程切换时会发生什么 就在Monitor.Enter(obj)之前;在你的LockOperation()中。 比如,第一个想要图像的线程构造一个新的锁,然后在它进入临界区之前进行线程切换。然后可能发生第二个线程在第一个线程之前进入临界区域。可能这不是一个真正的问题。

答案 4 :(得分:2)

你围绕一个简单的锁语句创建了一个非常复杂的包装器。创建一个url字典并为每个人创建一个锁定对象不是更好。你可以这样做。

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

您甚至可以通过直接锁定objLink对象来简化这一过程,因为它可能是GetUrl(“Url”)字符串实例。 (你必须锁定静态字符串列表)

如果非常容易出错,那么您就是原始代码。如果代码:

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

如果原始finally代码抛出异常,则会留下无效的LockObject状态。

答案 5 :(得分:0)

我在Bardock.Utils包中添加了一个由@ eugene-beresovsky answer启发的解决方案。

用法:

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());
        }
    }
}

答案 6 :(得分:0)

这是我实现此锁定架构的方式:

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;
        });
    }
}

并像这样使用:

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

void UseLocker()
{
     locker.ExecuteSynchronized("some string", () => DoSomething());
}

答案 7 :(得分:-1)

在大多数情况下,当你认为你需要锁时,你却不需要。相反,尝试使用线程安全的数据结构(例如Queue)来处理锁定。

例如,在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)

答案 8 :(得分:-1)

现在我有同样的问题。我决定使用简单的解决方案:避免死锁创建一个具有固定唯一前缀的新字符串,并将其用作锁定对象:

lock(string.Intern("uniqueprefix" + url))
{
//
}