Подтвердить что ты не робот

Блокировка интернированной строки?

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

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

Можно ли заблокировать key? Будет ли поддержка интернирования здесь?

После прочтения .NET 2.0 строка интернирования внутри 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;
    }
}

Является ли указанный выше поток реализации безопасным?

4b9b3361

Ответ 1

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

  • другие части вашей базы кода могут блокировать одни и те же интернированные экземпляры строк для разных целей, что приводит к только проблемам с производительностью, если повезет, и взаимоблокировкам, если повезет (потенциально только в будущем, поскольку код база растет, расширяясь кодами, не подозревая о вашем шаблоне блокировки String.Intern) - обратите внимание, что это включает блокировки на той же интернированной строке , даже если они в разных AppDomains, что потенциально приводит к взаимоблокировкам между приложениями.
  • невозможно восстановить внутреннюю память, если вы решили это сделать
  • 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;
}

Benchmark

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

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

Результаты

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

Тот факт, что InternConcurrentSafe работает так же быстро, как InternConcurrent, имеет смысл в свете того, что эти цифры после прогрева (см. выше 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());
    }
}

Ответ 2

Вариант ответа Даниэль...

Вместо создания нового объекта блокировки для каждой отдельной строки вы можете использовать небольшой набор блокировок, выбрав, какую блокировку использовать в зависимости от строкового хэш-кода. Это будет означать меньшее давление в ГК, если у вас потенциально есть тысячи или миллионы ключей, и должно позволить достаточно гранулярности избежать серьезной блокировки (возможно, после нескольких настроек, если это необходимо).

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

Я бы пошел с прагматичным подходом и использовал фиктивную переменную.
Если по какой-либо причине это невозможно, я бы использовал 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;
    }
}

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

Ответ 4

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

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

object myLock = new object();

Ответ 5

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

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

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

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

Ответ 6

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