19

更新:如果此方法不是线程安全的,这是可以接受的,但我有兴趣了解如何使其成为线程安全的。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+ 个线程将我的条件评估为 true,但我不知道要锁定什么。我读过的许多关于锁定的示例(例如Jon Skeet 的文章)都建议使用仅用于锁定的“虚拟”私有变量。在这种情况下这是不可能的,因为在编译时密钥是未知的。我知道我可以通过为 each 使用相同的锁来轻松地使这个线程安全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;
    }
}

上面的实现线程安全吗?

4

6 回答 6

23

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

  1. 您的代码库的其他部分可能会出于不同的目的锁定相同的实习字符串实例,如果幸运的话,只会导致性能问题,如果不幸运的话会导致死锁(可能只会在将来,随着代码库的增长,被不知道您的编码人员扩展String.Intern锁定模式) - 请注意,这包括对同一实习字符串的锁定,即使它们位于不同的 AppDomains 中,也可能导致跨 AppDomain 死锁
  2. 万一你决定这样做,你就不可能收回被拘留的记忆
  3. String.Intern()是缓慢的

要解决所有这 3 个问题,您可以实现Intern() 与特定锁定目的相关联的自己的问题,即不要将其用作全局通用字符串内部

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. 中提到的问题的影响。

为了比较各种实习字符串方式的性能,我还尝试了以下 2 种方法,以及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 位。

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

结果

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

鉴于这些数字是在热身之后的事实(见上文 NB),这一事实InternConcurrentSafeInternConcurrent有道理的,因此实际上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))
{
    ...

注意

再想一想,如果你只想锁定它,就不需要返回一个类型的对象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());
    }
}
于 2013-10-15T07:22:15.287 回答
6

丹尼尔答案的变体......

与其为每个字符串创建一个新的锁对象,您可以共享一组小的锁,根据字符串的哈希码选择要使用的锁。如果您可能拥有数千或数百万个键,这将意味着更少的 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 的答案。)

于 2011-08-08T15:22:01.420 回答
3

永远不要锁在弦上。特别是那些被实习的人。请参阅此博客条目,了解锁定实习字符串的危险。

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

object myLock = new object();
于 2011-08-08T14:35:13.847 回答
3

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

如果情况只是缓存常见的静态/只读内容,那么不要为了保存可能发生的奇数冲突而进行同步。(假设碰撞是良性的。)

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

如果您想阻止多次发生昂贵的调用,那么您可以将加载逻辑撕成一个新的类,按照 Oded 的回答在内部锁定对象上LoadMillionsOfRecords调用和锁定一次。.Load

于 2011-08-08T14:37:01.197 回答
3

我会采用务实的方法并使用虚拟变量。
如果由于某种原因这不可能,我会使用Dictionary<TKey, TValue>withkey作为键和一个虚拟对象作为值并锁定该值,因为字符串不适合锁定:

private object _syncRoot = new Object();
private Dictionary<string, object> _syncRoots = new Dictionary<string, object>();

public static T CheckCache<T>(string key, Func<T> fn, DateTime expires)
{
    object keySyncRoot;
    lock(_syncRoot)
    {

        if(!_syncRoots.TryGetValue(key, out keySyncRoot))
        {
            keySyncRoot = new object();
            _syncRoots[key] = keySyncRoot;
        }
    }
    lock(keySyncRoot)
    {

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

然而,在大多数情况下,这是矫枉过正和不必要的微优化。

于 2011-08-08T14:37:32.303 回答
0

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

用法:

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

string key = ...;

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

解决方案代码

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());
        }
    }
}
于 2015-06-30T21:03:28.117 回答