锁定一个实习的字符串?

时间:2011-08-08 14:32:24

标签: c# locking thread-safety higher-order-functions

更新:如果此方法不是线程安全的,可以接受,但我有兴趣了解如何使其保持线程安全。另外,如果可以避免,我不想为key的所有值锁定单个对象。

原始问题:假设我想编写一个带有键和函数的高阶函数,并检查对象是否已使用给定的密钥进行高速缓存。如果是,则返回缓存的值。否则,运行给定的函数并缓存并返回结果。

以下是我的代码的简化版本:

public static T CheckCache<T>(string key, Func<T> fn, DateTime expires)
{
    object cache = HttpContext.Current.Cache.Get(key);
    //clearly not thread safe, two threads could both evaluate the below condition as true
    //what can I lock on since the value of "key" may not be known at compile time?
    if (cache == null)
    {
        T result = fn();
        HttpContext.Current.Cache.Insert(key, result, null, expires, Cache.NoSlidingExpiration);
        return result;
    }
    else
        return (T)cache;
}

另外,假设我在编译时不知道key的所有可能值。

如何使这个线程安全?我知道我需要在这里引入锁定,以防止1+线程将我的条件评估为真,但我不知道要锁定什么。我读过的许多关于锁定的例子(例如Jon Skeet's article)建议使用仅用于锁定的“虚拟”私有变量。在这种情况下,这是不可能的,因为密钥在编译时是未知的。我知道我可以通过为每个key使用相同的锁来轻松地使这个线程安全,但这可能是浪费。

现在,我的主要问题是:

是否可以锁定key?字符串实习会有帮助吗?

在阅读.NET 2.0 string interning inside out之后,我明白我可以显式调用String.Intern()来获取从字符串值到字符串实例的1对1映射。 这是否适合锁定?让我们将上面的代码更改为:

public static T CheckCache<T>(string key, Func<T> fn, DateTime expires)
{
    //check for the scenario where two strings with the same value are stored at different memory locations
    key = String.Intern(key); 
    lock (key) //is this object suitable for locking?
    {
        object cache = HttpContext.Current.Cache.Get(key);
        if (cache == null)
        {
            T result = fn();
            HttpContext.Current.Cache.Insert(key, result, null, expires, Cache.NoSlidingExpiration);
            return result;
        }
        else
            return (T)cache;
    }
}

上述实现线程是否安全?

6 个答案:

答案 0 :(得分:21)

@ wsanville自己的解决方案的问题,部分在前面提到过:

  1. 您的代码库的其他部分可能为了不同的目的锁定相同的实习字符串实例,如果幸运则仅导致性能问题,如果不幸则导致死锁(可能仅在将来,作为代码base增长,由编码器扩展而不知道你的String.Intern锁定模式) - 请注意,这包括锁定在同一个实际字符串even if they are in different AppDomains上,可能导致跨AppDomain死锁
  2. 如果您决定这样做, 无法收回实习内存
  3. String.Intern()很慢
  4. 要解决所有这三个问题,您可以实施自己的Intern() ,这与您的特定锁定目的相关,即不要将其用作全局通用 string interner

    private static readonly ConcurrentDictionary<string, string> concSafe = 
        new ConcurrentDictionary<string, string>();
    static string InternConcurrentSafe(string s)
    {
        return concSafe.GetOrAdd(s, String.Copy);
    }
    

    我调用了这个方法...Safe(),因为在实习时我不会存储传入的String实例,因为这可能是是一个已经被拘禁的String,使其受到上述1.中提到的问题的影响。

    为了比较各种实习字符串方式的性能,我还尝试了以下两种方法,以及String.Intern

    private static readonly ConcurrentDictionary<string, string> conc = 
        new ConcurrentDictionary<string, string>();
    static string InternConcurrent(string s)
    {
        return conc.GetOrAdd(s, s);
    }
    
    private static readonly Dictionary<string, string> locked = 
        new Dictionary<string, string>(5000);
    static string InternLocked(string s)
    {
        string interned;
        lock (locked)
            if (!locked.TryGetValue(s, out interned))
                interned = locked[s] = s;
        return interned;
    }
    

    <强>基准

    100个线程,每个线程随机选择5000个不同字符串(每个包含8个数字)中的一个50000次,然后调用相应的实习方法。充分预热后的所有数值。这是4核i5上的Windows 7,64位。

    N.B。预热上述设置意味着在预热后,不会有写入到相应的实习词典,但只有读取。这是我对手头的用例感兴趣,但不同的写入/读取比率可能会影响结果。

    <强>结果

    • String.Intern():2032 ms
    • InternLocked():1245 ms
    • InternConcurrent():458 ms
    • InternConcurrentSafe():453 ms

    InternConcurrentSafeInternConcurrent一样快的事实是有道理的,因为这些数字是在升温之后(见上文NB),所以实际上没有或只有少数几个在测试期间调用String.Copy

    <小时/> 为了正确封装它,创建一个这样的类:

    public class StringLocker
    {
        private readonly ConcurrentDictionary<string, string> _locks =
            new ConcurrentDictionary<string, string>();
    
        public string GetLockObject(string s)
        {
            return _locks.GetOrAdd(s, String.Copy);
        }
    }
    

    并且在为您可能拥有的每个用例实例化一个StringLocker之后,就像调用

    一样简单
    lock(myStringLocker.GetLockObject(s))
    {
        ...
    

    <强> N.B。

    再次思考,无需返回string 类型的对象,如果你想做的就是锁定它,那么复制字符是完全没必要的,以下将会表现优于上课。

    public class StringLocker
    {
        private readonly ConcurrentDictionary<string, object> _locks =
            new ConcurrentDictionary<string, object>();
    
        public object GetLockObject(string s)
        {
            return _locks.GetOrAdd(s, k => new object());
        }
    }
    

答案 1 :(得分:5)

Daniel's answer的变体......

您可以共享一小组锁,而不是为每个单独的字符串创建一个新的锁对象,根据字符串的哈希码选择要使用的锁。如果您可能拥有数千或数百万个密钥,这将意味着更少的GC压力,并且应该允许足够的粒度以避免任何严重阻塞(可能在经过一些调整后,如果需要)。

public static T CheckCache<T>(string key, Func<T> fn, DateTime expires)
{
    object cached = HttpContext.Current.Cache[key];
    if (cached != null)
        return (T)cached;

    int stripeIndex = (key.GetHashCode() & 0x7FFFFFFF) % _stripes.Length;

    lock (_stripes[stripeIndex])
    {
        T result = fn();
        HttpContext.Current.Cache.Insert(key, result, null, expires,
                                         Cache.NoSlidingExpiration);
        return result;
    }
}

// share a set of 32 locks
private static readonly object[] _stripes = Enumerable.Range(0, 32)
                                                      .Select(x => new object())
                                                      .ToArray();

这将允许您通过更改_stripes数组中的元素数量来调整锁定粒度以满足您的特定需求。 (但是,如果你需要接近每个字符串的一个锁定粒度,那么你最好不要使用Daniel的答案。)

答案 2 :(得分:3)

我会采用实用的方法并使用虚拟变量 如果由于某种原因无法做到这一点,我会使用Dictionary<TKey, TValue>作为键{false}和伪对象作为值并锁定该值,因为字符串不适合锁定:

key

然而,在大多数情况下,这是过度杀伤和不必要的微观优化。

答案 3 :(得分:2)

永远不要锁定字符串。尤其是那些被拘禁的人。有关锁定实习字符串的危险,请参阅this blog entry

只需创建一个新对象并锁定它:

object myLock = new object();

答案 4 :(得分:2)

According to the documentation,Cache类型是线程安全的。因此,不同步自己的缺点是,在创建项目时,可能会在其他线程意识到不需要创建它之前创建几次。

如果情况只是缓存常见的静态/只读事物,那么不要只是为了保存可能发生的奇怪的几次冲突而烦扰同步。 (假设碰撞是良性的。)

锁定对象不是特定于字符串的,它将特定于您需要的锁的粒度。在这种情况下,您尝试锁定对缓存的访问,因此一个对象将服务锁定缓存。锁定特定键的想法并不是锁定通常所关注的概念。

如果你想多次停止昂贵的调用,那么你可以将加载逻辑转换为新的类LoadMillionsOfRecords,调用.Load并按照Oded的内部锁定对象锁定一次答案。

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