Блокировка интернированной строки? - PullRequest
15 голосов
/ 08 августа 2011

Обновление: Допустимо, если этот метод не является потокобезопасным, но мне интересно узнать, как сделать его безопасным для потоков.Кроме того, я не хочу блокировать один объект для всех значений key, если я могу избежать этого.

Оригинальный вопрос: Предположим, я хочу написать функцию более высокого порядка, котораяберет ключ и функцию и проверяет, был ли объект кэширован с данным ключом.Если is имеет, кэшированное значение возвращается.В противном случае данная функция запускается, а результат кэшируется и возвращается.

Вот упрощенная версия моего кода:

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+ оценивали мое состояние как истинное, но я не знаю, что блокировать.Во многих примерах, которые я читал о блокировке (например, статья Джона Скита ), рекомендуется использовать "фиктивную" закрытую переменную, которая используется только для блокировки.Это невозможно в этом случае, потому что ключи неизвестны во время компиляции.Я знаю, что мог бы тривиально сделать этот поток безопасным, используя одну и ту же блокировку для каждого key, но это может быть расточительным.

Теперь мой главный вопрос:

Можно ли заблокировать на key?Поможет ли здесь интернирование строк?

После прочтения .NET 2.0 интернирование наизнанку я понимаю, что могу явно вызвать 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 ]

21 голосов
/ 15 октября 2013

Проблемы с собственным решением @ wsanville, частично упомянутое ранее:

  1. другие части вашей базы кода могут блокировать одни и те же экземпляры интернированных строк для различных целей , вызывая только производительностьпроблемы, если повезет, и взаимоблокировки, если не повезло (возможно, только в будущем, по мере роста базы кода, расширяемой кодировщиками, не подозревающими о вашем String.Intern шаблоне блокировки) - обратите внимание, что это включает в себя блокировки для той же самой интернированной строки даже если они находятся в разных доменах приложений , что может привести к взаимным блокировкам между доменами приложений
  2. невозможно для вас восстановить внутреннюю память в случае, если вы решили это сделатьтак что
  3. String.Intern() медленно

Чтобы решить все эти 3 проблемы, вы можете реализовать свои собственные Intern() , которые вы привязываете к своемуконкретное назначение блокировки , т.е. не использовать его как глобальный универсальный string string :

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

Benchmark

100 потоков, каждый из которых выбирался случайным образом50000 раз одну из 5000 разных строк (каждая из которых содержит 8 цифр), а затем вызывать соответствующий метод intern.Все значения после прогрева достаточно.Это Windows 7, 64-битная, на 4core i5.

NB. Прогрев вышеописанной настройки подразумевает, что после прогрева не будет никаких записей в соответствующие международные словари, нотолько читает .Это то, что меня интересовало для рассматриваемого варианта использования, но различные соотношения записи / чтения, вероятно, повлияют на результаты.

Результаты

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

Тот факт, что InternConcurrentSafe так же быстр, как и InternConcurrent, имеет смысл в свете того факта, что эти цифры после прогрева (см. Выше NB), поэтому на самом деле нет или только несколько вызовов String.Copy во времяtest.


Чтобы правильно инкапсулировать это, создайте класс, подобный следующему:
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))
{
    ...

NB

Подумав еще раз, нет необходимости возвращать объект типа 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());
    }
}
5 голосов
/ 08 августа 2011

Вариант Ответ Даниэля ...

Вместо того, чтобы создавать новый объект блокировки для каждой отдельной строки, вы можете поделиться небольшим набором блокировок, выбирая, какую блокировку использовать, в зависимости от хеш-кода строки. Это будет означать меньшее давление 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. (Однако, если вам нужна гранулярность, близкая к одной блокировке на строку, то вам лучше пойти с ответом Даниэля.)

3 голосов
/ 08 августа 2011

Я бы пошел с прагматическим подходом и использовал бы фиктивную переменную.
Если это невозможно по какой-либо причине, я бы использовал Dictionary<TKey, TValue> с key в качестве ключа и фиктивный объект в качестве значения и блокировку этого значения, потому что строки не подходят для блокировки:

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

Однако в большинстве случаев это излишнее количество и ненужная микрооптимизация.

2 голосов
/ 08 августа 2011

Согласно документации , тип Cache является поточно-ориентированным.Поэтому недостатком для того, чтобы не синхронизировать себя, является то, что при создании элемента он может быть создан несколько раз, прежде чем другие потоки поймут, что им не нужно его создавать.

Если ситуация заключается в простом кешированииобщие статические / доступные только для чтения вещи, тогда не беспокойтесь о синхронизации просто чтобы сохранить несколько нечетных коллизий, которые могут произойти.(Предполагая, что столкновения являются доброкачественными.)

Объект блокировки не будет специфичным для строк, он будет зависеть от степени детализации требуемой блокировки.В этом случае вы пытаетесь заблокировать доступ к кешу, чтобы один объект обслуживал блокировку кеша.Идея блокировки конкретного входящего ключа не связана с концепцией блокировки.

Если вы хотите, чтобы дорогие вызовы не повторялись несколько раз, вы можете разорвать логику загрузки вновый класс LoadMillionsOfRecords, вызов .Load и однократная блокировка внутреннего объекта блокировки согласно ответу Одеда.

2 голосов
/ 08 августа 2011

Никогда не блокируйте струны.В частности на тех, кто интернирован.См. эту запись в блоге об опасности блокировки интернированных строк.

Просто создайте новый объект и заблокируйте его:

object myLock = new object();
0 голосов
/ 01 июля 2015

Я добавил решение в 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());
        }
    }
}
...